Skip to main content

oxirs_core/concurrent/
parallel_batch.rs

1//! Parallel batch processing for high-throughput RDF operations
2//!
3//! This module provides a parallel batch processor with work-stealing queues,
4//! configurable thread pools, and progress tracking for efficient RDF data processing.
5
6use crate::model::{Object, Predicate, Subject, Triple};
7use crate::OxirsError;
8use crossbeam_deque::Injector;
9use parking_lot::{Mutex, RwLock};
10#[cfg(feature = "parallel")]
11use rayon::prelude::*;
12use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
13use std::sync::{Arc, Barrier};
14use std::thread;
15use std::time::{Duration, Instant};
16
17/// Type alias for transform functions
18type TransformFn = Arc<dyn Fn(&Triple) -> Option<Triple> + Send + Sync>;
19
20/// Batch operation types
21#[derive(Clone)]
22pub enum BatchOperation {
23    /// Insert a collection of triples
24    Insert(Vec<Triple>),
25    /// Remove a collection of triples
26    Remove(Vec<Triple>),
27    /// Execute a query with pattern matching
28    Query {
29        subject: Option<Subject>,
30        predicate: Option<Predicate>,
31        object: Option<Object>,
32    },
33    /// Transform triples using a function
34    Transform(TransformFn),
35}
36
37impl std::fmt::Debug for BatchOperation {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            BatchOperation::Insert(triples) => write!(f, "Insert({} triples)", triples.len()),
41            BatchOperation::Remove(triples) => write!(f, "Remove({} triples)", triples.len()),
42            BatchOperation::Query {
43                subject,
44                predicate,
45                object,
46            } => {
47                write!(f, "Query({subject:?}, {predicate:?}, {object:?})")
48            }
49            BatchOperation::Transform(_) => write!(f, "Transform(function)"),
50        }
51    }
52}
53
54/// Progress callback for tracking batch operations
55pub type ProgressCallback = Box<dyn Fn(usize, usize) + Send + Sync>;
56
57/// Configuration for parallel batch processing
58#[derive(Debug, Clone)]
59pub struct BatchConfig {
60    /// Number of worker threads (defaults to number of CPU cores)
61    pub num_threads: Option<usize>,
62    /// Size of each batch for processing
63    pub batch_size: usize,
64    /// Maximum queue size before applying backpressure
65    pub max_queue_size: usize,
66    /// Timeout for batch operations
67    pub timeout: Option<Duration>,
68    /// Enable progress tracking
69    pub enable_progress: bool,
70}
71
72impl Default for BatchConfig {
73    fn default() -> Self {
74        let num_cpus = std::thread::available_parallelism()
75            .map(|n| n.get())
76            .unwrap_or(1);
77        BatchConfig {
78            num_threads: None,
79            batch_size: 1000,
80            max_queue_size: num_cpus * 10000,
81            timeout: None,
82            enable_progress: true,
83        }
84    }
85}
86
87impl BatchConfig {
88    /// Create a config optimized for the current system
89    pub fn auto() -> Self {
90        let num_cpus = std::thread::available_parallelism()
91            .map(|n| n.get())
92            .unwrap_or(1);
93        let total_memory = sys_info::mem_info()
94            .map(|info| info.total)
95            .unwrap_or(8 * 1024 * 1024); // 8GB default
96
97        // Adjust batch size based on available memory
98        let batch_size = if total_memory > 16 * 1024 * 1024 {
99            5000
100        } else if total_memory > 8 * 1024 * 1024 {
101            2000
102        } else {
103            1000
104        };
105
106        BatchConfig {
107            num_threads: Some(num_cpus),
108            batch_size,
109            max_queue_size: num_cpus * batch_size * 10,
110            timeout: None,
111            enable_progress: true,
112        }
113    }
114}
115
116/// Statistics for batch processing
117#[derive(Debug, Default)]
118pub struct BatchStats {
119    pub total_processed: AtomicUsize,
120    pub total_succeeded: AtomicUsize,
121    pub total_failed: AtomicUsize,
122    pub processing_time_ms: AtomicUsize,
123}
124
125impl BatchStats {
126    /// Get a summary of the statistics
127    pub fn summary(&self) -> BatchStatsSummary {
128        BatchStatsSummary {
129            total_processed: self.total_processed.load(Ordering::Relaxed),
130            total_succeeded: self.total_succeeded.load(Ordering::Relaxed),
131            total_failed: self.total_failed.load(Ordering::Relaxed),
132            processing_time_ms: self.processing_time_ms.load(Ordering::Relaxed),
133        }
134    }
135}
136
137#[derive(Debug, Clone)]
138pub struct BatchStatsSummary {
139    pub total_processed: usize,
140    pub total_succeeded: usize,
141    pub total_failed: usize,
142    pub processing_time_ms: usize,
143}
144
145/// Parallel batch processor with work-stealing queues
146pub struct ParallelBatchProcessor {
147    config: BatchConfig,
148    /// Global work queue (injector)
149    injector: Arc<Injector<BatchOperation>>,
150    /// Cancellation flag
151    cancelled: Arc<AtomicBool>,
152    /// Processing statistics
153    stats: Arc<BatchStats>,
154    /// Progress callback
155    progress_callback: Arc<Mutex<Option<ProgressCallback>>>,
156    /// Error accumulator
157    errors: Arc<RwLock<Vec<OxirsError>>>,
158}
159
160impl ParallelBatchProcessor {
161    /// Create a new parallel batch processor
162    pub fn new(config: BatchConfig) -> Self {
163        let injector = Arc::new(Injector::new());
164
165        ParallelBatchProcessor {
166            config,
167            injector,
168            cancelled: Arc::new(AtomicBool::new(false)),
169            stats: Arc::new(BatchStats::default()),
170            progress_callback: Arc::new(Mutex::new(None)),
171            errors: Arc::new(RwLock::new(Vec::new())),
172        }
173    }
174
175    /// Set a progress callback
176    pub fn set_progress_callback<F>(&self, callback: F)
177    where
178        F: Fn(usize, usize) + Send + Sync + 'static,
179    {
180        *self.progress_callback.lock() = Some(Box::new(callback));
181    }
182
183    /// Cancel ongoing operations
184    pub fn cancel(&self) {
185        self.cancelled.store(true, Ordering::SeqCst);
186    }
187
188    /// Check if operations are cancelled
189    pub fn is_cancelled(&self) -> bool {
190        self.cancelled.load(Ordering::SeqCst)
191    }
192
193    /// Get current statistics
194    pub fn stats(&self) -> BatchStatsSummary {
195        self.stats.summary()
196    }
197
198    /// Get accumulated errors
199    pub fn errors(&self) -> Vec<OxirsError> {
200        self.errors.read().clone()
201    }
202
203    /// Clear accumulated errors
204    pub fn clear_errors(&self) {
205        self.errors.write().clear();
206    }
207
208    /// Submit a batch operation
209    pub fn submit(&self, operation: BatchOperation) -> Result<(), OxirsError> {
210        // Check queue size for backpressure
211        if self.injector.len() > self.config.max_queue_size {
212            return Err(OxirsError::Store("Queue is full".to_string()));
213        }
214
215        self.injector.push(operation);
216        Ok(())
217    }
218
219    /// Submit multiple operations
220    pub fn submit_batch(&self, operations: Vec<BatchOperation>) -> Result<(), OxirsError> {
221        // Check if adding these operations would exceed queue size
222        if self.injector.len() + operations.len() > self.config.max_queue_size {
223            return Err(OxirsError::Store("Queue would overflow".to_string()));
224        }
225
226        for op in operations {
227            self.injector.push(op);
228        }
229        Ok(())
230    }
231
232    /// Process operations with the given executor
233    pub fn process<E, R>(&self, executor: E) -> Result<Vec<R>, OxirsError>
234    where
235        E: Fn(BatchOperation) -> Result<R, OxirsError> + Send + Sync + 'static,
236        R: Send + 'static,
237    {
238        let start_time = Instant::now();
239        let num_threads = self.config.num_threads.unwrap_or_else(|| {
240            std::thread::available_parallelism()
241                .map(|n| n.get())
242                .unwrap_or(1)
243        });
244        let barrier = Arc::new(Barrier::new(num_threads + 1));
245        let executor = Arc::new(executor);
246        let results = Arc::new(Mutex::new(Vec::new()));
247
248        // Reset cancellation flag
249        self.cancelled.store(false, Ordering::SeqCst);
250
251        // Spawn worker threads
252        let handles: Vec<_> = (0..num_threads)
253            .map(|_worker_id| {
254                let injector = self.injector.clone();
255                let cancelled = self.cancelled.clone();
256                let stats = self.stats.clone();
257                let executor = executor.clone();
258                let results = results.clone();
259                let errors = self.errors.clone();
260                let barrier = barrier.clone();
261                let progress_callback = self.progress_callback.clone();
262                let enable_progress = self.config.enable_progress;
263
264                thread::spawn(move || {
265                    // Wait for all threads to be ready
266                    barrier.wait();
267
268                    loop {
269                        // Check for cancellation
270                        if cancelled.load(Ordering::SeqCst) {
271                            break;
272                        }
273
274                        // Try to get work from global queue
275                        let task = loop {
276                            match injector.steal() {
277                                crossbeam_deque::Steal::Success(task) => break Some(task),
278                                crossbeam_deque::Steal::Empty => break None,
279                                crossbeam_deque::Steal::Retry => continue,
280                            }
281                        };
282
283                        match task {
284                            Some(operation) => {
285                                // Process the operation
286                                let processed =
287                                    stats.total_processed.fetch_add(1, Ordering::Relaxed) + 1;
288
289                                // Report progress
290                                if enable_progress && processed % 100 == 0 {
291                                    if let Some(callback) = &*progress_callback.lock() {
292                                        let total = injector.len() + processed;
293                                        callback(processed, total);
294                                    }
295                                }
296
297                                match executor(operation) {
298                                    Ok(result) => {
299                                        stats.total_succeeded.fetch_add(1, Ordering::Relaxed);
300                                        results.lock().push(result);
301                                    }
302                                    Err(e) => {
303                                        stats.total_failed.fetch_add(1, Ordering::Relaxed);
304                                        errors.write().push(e);
305                                    }
306                                }
307                            }
308                            None => {
309                                // No work available, check if we're done
310                                if injector.is_empty() {
311                                    break;
312                                }
313                                // Brief sleep to avoid busy-waiting
314                                thread::sleep(Duration::from_micros(10));
315                            }
316                        }
317                    }
318                })
319            })
320            .collect();
321
322        // Signal all threads to start
323        barrier.wait();
324
325        // Wait for completion or timeout
326        if let Some(timeout) = self.config.timeout {
327            let deadline = Instant::now() + timeout;
328            for handle in handles {
329                let remaining = deadline.saturating_duration_since(Instant::now());
330                if remaining.is_zero() {
331                    self.cancel();
332                    return Err(OxirsError::Store("Operation timed out".to_string()));
333                }
334                // Note: We can't actually join with timeout in std, would need a different approach
335                handle
336                    .join()
337                    .map_err(|_| OxirsError::Store("Worker thread panicked".to_string()))?;
338            }
339        } else {
340            for handle in handles {
341                handle
342                    .join()
343                    .map_err(|_| OxirsError::Store("Worker thread panicked".to_string()))?;
344            }
345        }
346
347        // Record processing time
348        let elapsed = start_time.elapsed();
349        self.stats
350            .processing_time_ms
351            .store(elapsed.as_millis() as usize, Ordering::Relaxed);
352
353        // Check for errors
354        let errors = self.errors.read();
355        if !errors.is_empty() {
356            return Err(OxirsError::Store(format!(
357                "Batch processing failed with {} errors",
358                errors.len()
359            )));
360        }
361
362        // Extract results
363        let final_results = Arc::try_unwrap(results)
364            .map_err(|_| OxirsError::Store("Failed to extract results from Arc".to_string()))?
365            .into_inner();
366
367        Ok(final_results)
368    }
369
370    /// Process operations in parallel using rayon
371    #[cfg(feature = "parallel")]
372    pub fn process_rayon<E, R>(&self, executor: E) -> Result<Vec<R>, OxirsError>
373    where
374        E: Fn(BatchOperation) -> Result<R, OxirsError> + Send + Sync,
375        R: Send,
376    {
377        let start_time = Instant::now();
378
379        // Collect all operations from the queue
380        let mut operations = Vec::new();
381        loop {
382            match self.injector.steal() {
383                crossbeam_deque::Steal::Success(op) => {
384                    if self.is_cancelled() {
385                        return Err(OxirsError::Store("Operation cancelled".to_string()));
386                    }
387                    operations.push(op);
388                }
389                crossbeam_deque::Steal::Empty => break,
390                crossbeam_deque::Steal::Retry => continue,
391            }
392        }
393
394        // Configure rayon thread pool
395        let pool = rayon::ThreadPoolBuilder::new()
396            .num_threads(self.config.num_threads.unwrap_or_else(|| {
397                std::thread::available_parallelism()
398                    .map(|n| n.get())
399                    .unwrap_or(1)
400            }))
401            .build()
402            .map_err(|e| OxirsError::Store(format!("Failed to build thread pool: {e}")))?;
403
404        // Clone needed references
405        let cancelled = self.cancelled.clone();
406        let stats = self.stats.clone();
407        let errors = self.errors.clone();
408        let batch_size = self.config.batch_size;
409        let executor = Arc::new(executor);
410
411        // Process in parallel
412        let results = pool.install(move || {
413            operations
414                .into_par_iter()
415                .chunks(batch_size)
416                .map(move |chunk| {
417                    let mut chunk_results = Vec::new();
418                    for op in chunk {
419                        if cancelled.load(Ordering::SeqCst) {
420                            return Err(OxirsError::Store("Operation cancelled".to_string()));
421                        }
422
423                        stats.total_processed.fetch_add(1, Ordering::Relaxed);
424
425                        match executor(op) {
426                            Ok(result) => {
427                                stats.total_succeeded.fetch_add(1, Ordering::Relaxed);
428                                chunk_results.push(result);
429                            }
430                            Err(e) => {
431                                stats.total_failed.fetch_add(1, Ordering::Relaxed);
432                                errors.write().push(e.clone());
433                                return Err(e);
434                            }
435                        }
436                    }
437                    Ok(chunk_results)
438                })
439                .collect::<Result<Vec<_>, _>>()
440        })?;
441
442        // Flatten results
443        let results: Vec<R> = results.into_iter().flatten().collect();
444
445        // Record processing time
446        let elapsed = start_time.elapsed();
447        self.stats
448            .processing_time_ms
449            .store(elapsed.as_millis() as usize, Ordering::Relaxed);
450
451        Ok(results)
452    }
453}
454
455/// Helper functions for creating batch operations
456impl BatchOperation {
457    /// Create an insert operation
458    pub fn insert(triples: Vec<Triple>) -> Self {
459        BatchOperation::Insert(triples)
460    }
461
462    /// Create a remove operation
463    pub fn remove(triples: Vec<Triple>) -> Self {
464        BatchOperation::Remove(triples)
465    }
466
467    /// Create a query operation
468    pub fn query(
469        subject: Option<Subject>,
470        predicate: Option<Predicate>,
471        object: Option<Object>,
472    ) -> Self {
473        BatchOperation::Query {
474            subject,
475            predicate,
476            object,
477        }
478    }
479
480    /// Create a transform operation
481    pub fn transform<F>(f: F) -> Self
482    where
483        F: Fn(&Triple) -> Option<Triple> + Send + Sync + 'static,
484    {
485        BatchOperation::Transform(Arc::new(f))
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use crate::model::NamedNode;
493
494    fn create_test_triple(id: usize) -> Triple {
495        Triple::new(
496            Subject::NamedNode(
497                NamedNode::new(format!("http://subject/{id}")).expect("valid IRI from format"),
498            ),
499            Predicate::NamedNode(
500                NamedNode::new(format!("http://predicate/{id}")).expect("valid IRI from format"),
501            ),
502            Object::NamedNode(
503                NamedNode::new(format!("http://object/{id}")).expect("valid IRI from format"),
504            ),
505        )
506    }
507
508    #[test]
509    fn test_parallel_batch_processor() {
510        let config = BatchConfig::default();
511        let processor = ParallelBatchProcessor::new(config);
512
513        // Submit some operations
514        let operations: Vec<_> = (0..1000)
515            .map(|i| BatchOperation::insert(vec![create_test_triple(i)]))
516            .collect();
517
518        processor
519            .submit_batch(operations)
520            .expect("operation should succeed");
521
522        // Process with a simple executor
523        let results = processor
524            .process(|op| -> Result<usize, OxirsError> {
525                match op {
526                    BatchOperation::Insert(triples) => Ok(triples.len()),
527                    _ => Ok(0),
528                }
529            })
530            .expect("operation should succeed");
531
532        assert_eq!(results.len(), 1000);
533        assert_eq!(results.iter().sum::<usize>(), 1000);
534
535        let stats = processor.stats();
536        assert_eq!(stats.total_processed, 1000);
537        assert_eq!(stats.total_succeeded, 1000);
538        assert_eq!(stats.total_failed, 0);
539    }
540
541    #[test]
542    #[cfg(feature = "parallel")]
543    fn test_work_stealing() {
544        let config = BatchConfig {
545            num_threads: Some(4),
546            batch_size: 10,
547            ..Default::default()
548        };
549
550        let processor = ParallelBatchProcessor::new(config);
551
552        // Submit operations
553        for i in 0..100 {
554            processor
555                .submit(BatchOperation::insert(vec![create_test_triple(i)]))
556                .expect("operation should succeed");
557        }
558
559        // Process and verify work is distributed
560        let results = processor
561            .process_rayon(|op| -> Result<usize, OxirsError> {
562                // Simulate some work
563                thread::sleep(Duration::from_micros(100));
564                match op {
565                    BatchOperation::Insert(triples) => Ok(triples.len()),
566                    _ => Ok(0),
567                }
568            })
569            .expect("operation should succeed");
570
571        assert_eq!(results.len(), 100);
572        let stats = processor.stats();
573        assert_eq!(stats.total_processed, 100);
574    }
575
576    #[test]
577    fn test_error_handling() {
578        let config = BatchConfig::default();
579        let processor = ParallelBatchProcessor::new(config);
580
581        // Submit operations that will fail
582        for i in 0..10 {
583            processor
584                .submit(BatchOperation::insert(vec![create_test_triple(i)]))
585                .expect("operation should succeed");
586        }
587
588        // Process with failing executor
589        let result = processor.process(|_op| -> Result<(), OxirsError> {
590            Err(OxirsError::Store("Test error".to_string()))
591        });
592
593        assert!(result.is_err());
594        let stats = processor.stats();
595        assert_eq!(stats.total_failed, 10);
596        assert_eq!(processor.errors().len(), 10);
597    }
598
599    #[test]
600    fn test_cancellation() {
601        let config = BatchConfig::default();
602        let processor = Arc::new(ParallelBatchProcessor::new(config));
603
604        // Submit many operations
605        for i in 0..1000 {
606            processor
607                .submit(BatchOperation::insert(vec![create_test_triple(i)]))
608                .expect("operation should succeed");
609        }
610
611        // Start processing in a thread
612        let processor_thread = processor.clone();
613
614        let handle = thread::spawn(move || {
615            processor_thread.process(|op| -> Result<(), OxirsError> {
616                // Simulate slow processing
617                thread::sleep(Duration::from_millis(10));
618                match op {
619                    BatchOperation::Insert(_) => Ok(()),
620                    _ => Ok(()),
621                }
622            })
623        });
624
625        // Cancel after a short delay
626        thread::sleep(Duration::from_millis(50));
627        processor.cancel();
628
629        // Wait for completion
630        let _result = handle.join().expect("thread should not panic");
631
632        // Should have processed some but not all
633        let stats = processor.stats();
634        assert!(stats.total_processed < 1000);
635        assert!(processor.is_cancelled());
636    }
637
638    #[test]
639    fn test_progress_tracking() {
640        let config = BatchConfig::default();
641        let processor = ParallelBatchProcessor::new(config);
642
643        let progress_count = Arc::new(AtomicUsize::new(0));
644        let progress_count_clone = progress_count.clone();
645
646        processor.set_progress_callback(move |current, _total| {
647            progress_count_clone.fetch_add(1, Ordering::Relaxed);
648            println!("Progress: {current}/{_total}");
649        });
650
651        // Submit operations
652        for i in 0..500 {
653            processor
654                .submit(BatchOperation::insert(vec![create_test_triple(i)]))
655                .expect("operation should succeed");
656        }
657
658        // Process
659        processor
660            .process(|op| -> Result<(), OxirsError> {
661                match op {
662                    BatchOperation::Insert(_) => Ok(()),
663                    _ => Ok(()),
664                }
665            })
666            .expect("operation should succeed");
667
668        // Should have received progress updates
669        assert!(progress_count.load(Ordering::Relaxed) > 0);
670    }
671}