1use 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
17type TransformFn = Arc<dyn Fn(&Triple) -> Option<Triple> + Send + Sync>;
19
20#[derive(Clone)]
22pub enum BatchOperation {
23 Insert(Vec<Triple>),
25 Remove(Vec<Triple>),
27 Query {
29 subject: Option<Subject>,
30 predicate: Option<Predicate>,
31 object: Option<Object>,
32 },
33 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
54pub type ProgressCallback = Box<dyn Fn(usize, usize) + Send + Sync>;
56
57#[derive(Debug, Clone)]
59pub struct BatchConfig {
60 pub num_threads: Option<usize>,
62 pub batch_size: usize,
64 pub max_queue_size: usize,
66 pub timeout: Option<Duration>,
68 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 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); 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#[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 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
145pub struct ParallelBatchProcessor {
147 config: BatchConfig,
148 injector: Arc<Injector<BatchOperation>>,
150 cancelled: Arc<AtomicBool>,
152 stats: Arc<BatchStats>,
154 progress_callback: Arc<Mutex<Option<ProgressCallback>>>,
156 errors: Arc<RwLock<Vec<OxirsError>>>,
158}
159
160impl ParallelBatchProcessor {
161 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 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 pub fn cancel(&self) {
185 self.cancelled.store(true, Ordering::SeqCst);
186 }
187
188 pub fn is_cancelled(&self) -> bool {
190 self.cancelled.load(Ordering::SeqCst)
191 }
192
193 pub fn stats(&self) -> BatchStatsSummary {
195 self.stats.summary()
196 }
197
198 pub fn errors(&self) -> Vec<OxirsError> {
200 self.errors.read().clone()
201 }
202
203 pub fn clear_errors(&self) {
205 self.errors.write().clear();
206 }
207
208 pub fn submit(&self, operation: BatchOperation) -> Result<(), OxirsError> {
210 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 pub fn submit_batch(&self, operations: Vec<BatchOperation>) -> Result<(), OxirsError> {
221 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 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 self.cancelled.store(false, Ordering::SeqCst);
250
251 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 barrier.wait();
267
268 loop {
269 if cancelled.load(Ordering::SeqCst) {
271 break;
272 }
273
274 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 let processed =
287 stats.total_processed.fetch_add(1, Ordering::Relaxed) + 1;
288
289 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 if injector.is_empty() {
311 break;
312 }
313 thread::sleep(Duration::from_micros(10));
315 }
316 }
317 }
318 })
319 })
320 .collect();
321
322 barrier.wait();
324
325 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 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 let elapsed = start_time.elapsed();
349 self.stats
350 .processing_time_ms
351 .store(elapsed.as_millis() as usize, Ordering::Relaxed);
352
353 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 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 #[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 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 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 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 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 let results: Vec<R> = results.into_iter().flatten().collect();
444
445 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
455impl BatchOperation {
457 pub fn insert(triples: Vec<Triple>) -> Self {
459 BatchOperation::Insert(triples)
460 }
461
462 pub fn remove(triples: Vec<Triple>) -> Self {
464 BatchOperation::Remove(triples)
465 }
466
467 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 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 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 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 for i in 0..100 {
554 processor
555 .submit(BatchOperation::insert(vec![create_test_triple(i)]))
556 .expect("operation should succeed");
557 }
558
559 let results = processor
561 .process_rayon(|op| -> Result<usize, OxirsError> {
562 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 for i in 0..10 {
583 processor
584 .submit(BatchOperation::insert(vec![create_test_triple(i)]))
585 .expect("operation should succeed");
586 }
587
588 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 for i in 0..1000 {
606 processor
607 .submit(BatchOperation::insert(vec![create_test_triple(i)]))
608 .expect("operation should succeed");
609 }
610
611 let processor_thread = processor.clone();
613
614 let handle = thread::spawn(move || {
615 processor_thread.process(|op| -> Result<(), OxirsError> {
616 thread::sleep(Duration::from_millis(10));
618 match op {
619 BatchOperation::Insert(_) => Ok(()),
620 _ => Ok(()),
621 }
622 })
623 });
624
625 thread::sleep(Duration::from_millis(50));
627 processor.cancel();
628
629 let _result = handle.join().expect("thread should not panic");
631
632 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 for i in 0..500 {
653 processor
654 .submit(BatchOperation::insert(vec![create_test_triple(i)]))
655 .expect("operation should succeed");
656 }
657
658 processor
660 .process(|op| -> Result<(), OxirsError> {
661 match op {
662 BatchOperation::Insert(_) => Ok(()),
663 _ => Ok(()),
664 }
665 })
666 .expect("operation should succeed");
667
668 assert!(progress_count.load(Ordering::Relaxed) > 0);
670 }
671}