Skip to main content

oxirs_core/concurrent/
batch_builder.rs

1//! Batch builder for accumulating and optimizing RDF operations
2//!
3//! This module provides a builder pattern for accumulating operations into
4//! optimal batches based on system resources and operation types.
5
6use crate::concurrent::parallel_batch::{BatchConfig, BatchOperation};
7use crate::model::{Object, Predicate, Subject, Triple};
8use crate::OxirsError;
9use parking_lot::Mutex;
10use std::collections::HashSet;
11use std::sync::Arc;
12
13/// Type alias for transform functions
14type TransformFn = Arc<dyn Fn(&Triple) -> Option<Triple> + Send + Sync>;
15
16/// Type alias for flush callback functions
17type FlushCallback = Arc<Mutex<Option<Box<dyn Fn(Vec<BatchOperation>) + Send + Sync>>>>;
18
19/// Operation coalescing strategy
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CoalescingStrategy {
22    /// No coalescing - operations are kept as-is
23    None,
24    /// Deduplicate operations (remove duplicates)
25    Deduplicate,
26    /// Merge compatible operations
27    Merge,
28    /// Optimize operation order for better cache locality
29    OptimizeOrder,
30}
31
32/// Batch builder configuration
33#[derive(Debug, Clone)]
34pub struct BatchBuilderConfig {
35    /// Maximum size of a single batch
36    pub max_batch_size: usize,
37    /// Maximum memory usage in bytes
38    pub max_memory_usage: usize,
39    /// Coalescing strategy
40    pub coalescing_strategy: CoalescingStrategy,
41    /// Auto-flush when batch is full
42    pub auto_flush: bool,
43    /// Group operations by type for better performance
44    pub group_by_type: bool,
45}
46
47impl Default for BatchBuilderConfig {
48    fn default() -> Self {
49        let total_memory = sys_info::mem_info()
50            .map(|info| info.total * 1024) // Convert to bytes
51            .unwrap_or(8 * 1024 * 1024 * 1024); // 8GB default
52
53        BatchBuilderConfig {
54            max_batch_size: 10000,
55            max_memory_usage: (total_memory as usize) / 10, // Use up to 10% of system memory
56            coalescing_strategy: CoalescingStrategy::Deduplicate,
57            auto_flush: true,
58            group_by_type: true,
59        }
60    }
61}
62
63impl BatchBuilderConfig {
64    /// Create configuration optimized for current system
65    pub fn auto() -> Self {
66        let num_cpus = std::thread::available_parallelism()
67            .map(|n| n.get())
68            .unwrap_or(1);
69        let mem_info = sys_info::mem_info().ok();
70
71        let (max_batch_size, max_memory_usage) = if let Some(info) = mem_info {
72            let total_mb = info.total / 1024;
73            if total_mb > 16384 {
74                // > 16GB
75                (50000, (info.total * 1024 / 8) as usize) // Large batches, use 1/8 of memory
76            } else if total_mb > 8192 {
77                // > 8GB
78                (20000, (info.total * 1024 / 10) as usize) // Medium batches, use 1/10 of memory
79            } else {
80                (5000, (info.total * 1024 / 20) as usize) // Small batches, use 1/20 of memory
81            }
82        } else {
83            (10000, 1024 * 1024 * 1024) // 1GB default
84        };
85
86        BatchBuilderConfig {
87            max_batch_size: max_batch_size * num_cpus / 4, // Scale with CPU count
88            max_memory_usage,
89            coalescing_strategy: CoalescingStrategy::Merge,
90            auto_flush: true,
91            group_by_type: true,
92        }
93    }
94}
95
96/// Statistics for batch building
97#[derive(Debug, Clone, Default)]
98pub struct BatchBuilderStats {
99    pub total_operations: usize,
100    pub coalesced_operations: usize,
101    pub deduplicated_operations: usize,
102    pub batches_created: usize,
103    pub estimated_memory_usage: usize,
104}
105
106/// Batch builder for accumulating operations
107pub struct BatchBuilder {
108    config: BatchBuilderConfig,
109    /// Insert operations
110    insert_buffer: Vec<Triple>,
111    insert_set: HashSet<Triple>,
112    /// Remove operations  
113    remove_buffer: Vec<Triple>,
114    remove_set: HashSet<Triple>,
115    /// Query operations
116    query_buffer: Vec<(Option<Subject>, Option<Predicate>, Option<Object>)>,
117    /// Transform operations
118    transform_buffer: Vec<TransformFn>,
119    /// Current estimated memory usage
120    estimated_memory: usize,
121    /// Statistics
122    stats: BatchBuilderStats,
123    /// Flush callback
124    flush_callback: FlushCallback,
125}
126
127impl BatchBuilder {
128    /// Create a new batch builder
129    pub fn new(config: BatchBuilderConfig) -> Self {
130        BatchBuilder {
131            config,
132            insert_buffer: Vec::new(),
133            insert_set: HashSet::new(),
134            remove_buffer: Vec::new(),
135            remove_set: HashSet::new(),
136            query_buffer: Vec::new(),
137            transform_buffer: Vec::new(),
138            estimated_memory: 0,
139            stats: BatchBuilderStats::default(),
140            flush_callback: Arc::new(Mutex::new(None)),
141        }
142    }
143
144    /// Create a batch builder with automatic configuration
145    pub fn auto() -> Self {
146        Self::new(BatchBuilderConfig::auto())
147    }
148
149    /// Set a callback to be called when batches are flushed
150    pub fn on_flush<F>(&mut self, callback: F)
151    where
152        F: Fn(Vec<BatchOperation>) + Send + Sync + 'static,
153    {
154        *self.flush_callback.lock() = Some(Box::new(callback));
155    }
156
157    /// Add an insert operation
158    pub fn insert(&mut self, triple: Triple) -> Result<(), OxirsError> {
159        self.stats.total_operations += 1;
160
161        // Apply coalescing
162        match self.config.coalescing_strategy {
163            CoalescingStrategy::None => {
164                self.estimated_memory += self.estimate_triple_size(&triple);
165                self.insert_buffer.push(triple);
166            }
167            CoalescingStrategy::Deduplicate | CoalescingStrategy::Merge => {
168                if self.insert_set.insert(triple.clone()) {
169                    self.insert_buffer.push(triple.clone());
170                    self.estimated_memory += self.estimate_triple_size(&triple);
171                } else {
172                    self.stats.deduplicated_operations += 1;
173                }
174            }
175            CoalescingStrategy::OptimizeOrder => {
176                // For optimize order, we'll sort later
177                if self.insert_set.insert(triple.clone()) {
178                    self.insert_buffer.push(triple.clone());
179                    self.estimated_memory += self.estimate_triple_size(&triple);
180                }
181            }
182        }
183
184        self.check_flush()?;
185        Ok(())
186    }
187
188    /// Add multiple insert operations
189    pub fn insert_batch(&mut self, triples: Vec<Triple>) -> Result<(), OxirsError> {
190        for triple in triples {
191            self.insert(triple)?;
192        }
193        Ok(())
194    }
195
196    /// Add a remove operation
197    pub fn remove(&mut self, triple: Triple) -> Result<(), OxirsError> {
198        self.stats.total_operations += 1;
199
200        // Apply coalescing
201        match self.config.coalescing_strategy {
202            CoalescingStrategy::None => {
203                self.estimated_memory += self.estimate_triple_size(&triple);
204                self.remove_buffer.push(triple);
205            }
206            CoalescingStrategy::Deduplicate | CoalescingStrategy::Merge => {
207                if self.remove_set.insert(triple.clone()) {
208                    self.remove_buffer.push(triple.clone());
209                    self.estimated_memory += self.estimate_triple_size(&triple);
210                } else {
211                    self.stats.deduplicated_operations += 1;
212                }
213            }
214            CoalescingStrategy::OptimizeOrder => {
215                if self.remove_set.insert(triple.clone()) {
216                    self.remove_buffer.push(triple.clone());
217                    self.estimated_memory += self.estimate_triple_size(&triple);
218                }
219            }
220        }
221
222        self.check_flush()?;
223        Ok(())
224    }
225
226    /// Add a query operation
227    pub fn query(
228        &mut self,
229        subject: Option<Subject>,
230        predicate: Option<Predicate>,
231        object: Option<Object>,
232    ) -> Result<(), OxirsError> {
233        self.stats.total_operations += 1;
234        self.query_buffer.push((subject, predicate, object));
235        self.estimated_memory += 128; // Rough estimate for query pattern
236
237        self.check_flush()?;
238        Ok(())
239    }
240
241    /// Add a transform operation
242    pub fn transform<F>(&mut self, f: F) -> Result<(), OxirsError>
243    where
244        F: Fn(&Triple) -> Option<Triple> + Send + Sync + 'static,
245    {
246        self.stats.total_operations += 1;
247        self.transform_buffer.push(Arc::new(f));
248        self.estimated_memory += 64; // Rough estimate for closure
249
250        self.check_flush()?;
251        Ok(())
252    }
253
254    /// Get current statistics
255    pub fn stats(&self) -> &BatchBuilderStats {
256        &self.stats
257    }
258
259    /// Get the current number of pending operations
260    pub fn pending_operations(&self) -> usize {
261        self.insert_buffer.len()
262            + self.remove_buffer.len()
263            + self.query_buffer.len()
264            + self.transform_buffer.len()
265    }
266
267    /// Check if we should flush based on size or memory constraints
268    fn check_flush(&mut self) -> Result<(), OxirsError> {
269        if self.config.auto_flush {
270            let should_flush = self.pending_operations() >= self.config.max_batch_size
271                || self.estimated_memory >= self.config.max_memory_usage;
272
273            if should_flush {
274                self.flush()?;
275            }
276        }
277        Ok(())
278    }
279
280    /// Estimate the memory size of a triple
281    fn estimate_triple_size(&self, triple: &Triple) -> usize {
282        // Rough estimation:
283        // - Each IRI/blank node: ~100 bytes
284        // - Each literal: string length + 50 bytes overhead
285        // - Triple structure: 24 bytes
286        24 + self.estimate_term_size(triple.subject())
287            + self.estimate_term_size(triple.predicate())
288            + self.estimate_object_size(triple.object())
289    }
290
291    fn estimate_term_size(&self, _term: &impl std::fmt::Display) -> usize {
292        100 // Simplified estimation
293    }
294
295    fn estimate_object_size(&self, _object: &Object) -> usize {
296        150 // Simplified estimation, literals can be larger
297    }
298
299    /// Flush all pending operations into batch operations
300    pub fn flush(&mut self) -> Result<Vec<BatchOperation>, OxirsError> {
301        let mut operations = Vec::new();
302
303        if self.config.coalescing_strategy == CoalescingStrategy::Merge {
304            self.apply_merge_coalescing();
305        }
306
307        // Group operations by type if configured
308        if self.config.group_by_type {
309            // Optimize order if requested
310            if self.config.coalescing_strategy == CoalescingStrategy::OptimizeOrder {
311                self.optimize_operation_order();
312            }
313
314            // Create batches from buffers
315            if !self.insert_buffer.is_empty() {
316                operations.extend(self.create_insert_batches());
317            }
318
319            if !self.remove_buffer.is_empty() {
320                operations.extend(self.create_remove_batches());
321            }
322
323            if !self.query_buffer.is_empty() {
324                operations.extend(self.create_query_batches());
325            }
326
327            if !self.transform_buffer.is_empty() {
328                operations.extend(self.create_transform_batches());
329            }
330        } else {
331            // Mix operation types in batches
332            operations = self.create_mixed_batches();
333        }
334
335        // Update statistics
336        self.stats.batches_created += operations.len();
337        self.stats.estimated_memory_usage = self.estimated_memory;
338
339        // Clear buffers
340        self.clear();
341
342        // Call flush callback if set
343        if let Some(callback) = &*self.flush_callback.lock() {
344            callback(operations.clone());
345        }
346
347        Ok(operations)
348    }
349
350    /// Apply merge coalescing to combine compatible operations
351    fn apply_merge_coalescing(&mut self) {
352        // Remove inserts that are immediately removed
353        if !self.insert_buffer.is_empty() && !self.remove_buffer.is_empty() {
354            let remove_set = &self.remove_set;
355            let original_len = self.insert_buffer.len();
356            self.insert_buffer
357                .retain(|triple| !remove_set.contains(triple));
358            let coalesced = original_len - self.insert_buffer.len();
359
360            if coalesced > 0 {
361                self.stats.coalesced_operations += coalesced;
362                // Also remove from remove buffer
363                let insert_set = &self.insert_set;
364                self.remove_buffer
365                    .retain(|triple| !insert_set.contains(triple));
366            }
367        }
368    }
369
370    /// Optimize operation order for better cache locality
371    fn optimize_operation_order(&mut self) {
372        // Sort by subject for better cache locality
373        self.insert_buffer.sort_by_key(|a| a.subject().to_string());
374
375        self.remove_buffer.sort_by_key(|a| a.subject().to_string());
376    }
377
378    /// Create insert batches respecting max batch size
379    fn create_insert_batches(&mut self) -> Vec<BatchOperation> {
380        let mut batches = Vec::new();
381        let mut current_batch = Vec::new();
382
383        for triple in self.insert_buffer.drain(..) {
384            current_batch.push(triple);
385            if current_batch.len() >= self.config.max_batch_size {
386                batches.push(BatchOperation::Insert(std::mem::take(&mut current_batch)));
387            }
388        }
389
390        if !current_batch.is_empty() {
391            batches.push(BatchOperation::Insert(current_batch));
392        }
393
394        batches
395    }
396
397    /// Create remove batches respecting max batch size
398    fn create_remove_batches(&mut self) -> Vec<BatchOperation> {
399        let mut batches = Vec::new();
400        let mut current_batch = Vec::new();
401
402        for triple in self.remove_buffer.drain(..) {
403            current_batch.push(triple);
404            if current_batch.len() >= self.config.max_batch_size {
405                batches.push(BatchOperation::Remove(std::mem::take(&mut current_batch)));
406            }
407        }
408
409        if !current_batch.is_empty() {
410            batches.push(BatchOperation::Remove(current_batch));
411        }
412
413        batches
414    }
415
416    /// Create query batches
417    fn create_query_batches(&mut self) -> Vec<BatchOperation> {
418        self.query_buffer
419            .drain(..)
420            .map(|(s, p, o)| BatchOperation::Query {
421                subject: s,
422                predicate: p,
423                object: o,
424            })
425            .collect()
426    }
427
428    /// Create transform batches
429    fn create_transform_batches(&mut self) -> Vec<BatchOperation> {
430        self.transform_buffer
431            .drain(..)
432            .map(BatchOperation::Transform)
433            .collect()
434    }
435
436    /// Create mixed batches with different operation types
437    fn create_mixed_batches(&mut self) -> Vec<BatchOperation> {
438        // This is a simplified implementation
439        // In a real scenario, you might want to interleave operations more intelligently
440        let mut operations = Vec::new();
441
442        operations.extend(self.create_insert_batches());
443        operations.extend(self.create_remove_batches());
444        operations.extend(self.create_query_batches());
445        operations.extend(self.create_transform_batches());
446
447        operations
448    }
449
450    /// Clear all buffers
451    fn clear(&mut self) {
452        self.insert_buffer.clear();
453        self.insert_set.clear();
454        self.remove_buffer.clear();
455        self.remove_set.clear();
456        self.query_buffer.clear();
457        self.transform_buffer.clear();
458        self.estimated_memory = 0;
459    }
460}
461
462/// Create a batch configuration from builder config
463impl From<&BatchBuilderConfig> for BatchConfig {
464    fn from(builder_config: &BatchBuilderConfig) -> Self {
465        BatchConfig {
466            batch_size: builder_config.max_batch_size,
467            ..Default::default()
468        }
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use crate::model::NamedNode;
476
477    fn create_test_triple(id: usize) -> Triple {
478        Triple::new(
479            Subject::NamedNode(
480                NamedNode::new(format!("http://subject/{id}")).expect("valid IRI from format"),
481            ),
482            Predicate::NamedNode(
483                NamedNode::new(format!("http://predicate/{id}")).expect("valid IRI from format"),
484            ),
485            Object::NamedNode(
486                NamedNode::new(format!("http://object/{id}")).expect("valid IRI from format"),
487            ),
488        )
489    }
490
491    #[test]
492    fn test_batch_builder_basic() {
493        let config = BatchBuilderConfig {
494            max_batch_size: 10,
495            auto_flush: false,
496            ..Default::default()
497        };
498
499        let mut builder = BatchBuilder::new(config);
500
501        // Add operations
502        for i in 0..25 {
503            builder
504                .insert(create_test_triple(i))
505                .expect("builder insert should succeed");
506        }
507
508        assert_eq!(builder.pending_operations(), 25);
509
510        // Flush and check batches
511        let batches = builder.flush().expect("flush should succeed");
512        assert_eq!(batches.len(), 3); // 10 + 10 + 5
513        assert_eq!(builder.pending_operations(), 0);
514    }
515
516    #[test]
517    fn test_deduplication() {
518        let config = BatchBuilderConfig {
519            coalescing_strategy: CoalescingStrategy::Deduplicate,
520            auto_flush: false,
521            ..Default::default()
522        };
523
524        let mut builder = BatchBuilder::new(config);
525
526        // Add duplicate triples
527        let triple = create_test_triple(1);
528        for _ in 0..5 {
529            builder
530                .insert(triple.clone())
531                .expect("builder insert should succeed");
532        }
533
534        assert_eq!(builder.pending_operations(), 1);
535        assert_eq!(builder.stats().deduplicated_operations, 4);
536    }
537
538    #[test]
539    fn test_merge_coalescing() {
540        let config = BatchBuilderConfig {
541            coalescing_strategy: CoalescingStrategy::Merge,
542            auto_flush: false,
543            ..Default::default()
544        };
545
546        let mut builder = BatchBuilder::new(config);
547
548        // Add insert then remove same triple
549        let triple = create_test_triple(1);
550        builder
551            .insert(triple.clone())
552            .expect("builder insert should succeed");
553        builder.remove(triple).expect("remove should succeed");
554
555        // After merge, both should be eliminated
556        let batches = builder.flush().expect("flush should succeed");
557        assert_eq!(batches.len(), 0);
558        assert_eq!(builder.stats().coalesced_operations, 1);
559    }
560
561    #[test]
562    fn test_auto_flush() {
563        let config = BatchBuilderConfig {
564            max_batch_size: 5,
565            auto_flush: true,
566            ..Default::default()
567        };
568
569        let flushed_batches = Arc::new(Mutex::new(Vec::new()));
570        let flushed_clone = flushed_batches.clone();
571
572        let mut builder = BatchBuilder::new(config);
573        builder.on_flush(move |batches| {
574            flushed_clone.lock().extend(batches);
575        });
576
577        // Add operations that trigger auto-flush
578        for i in 0..12 {
579            builder
580                .insert(create_test_triple(i))
581                .expect("builder insert should succeed");
582        }
583
584        // Should have auto-flushed twice
585        assert_eq!(flushed_batches.lock().len(), 2);
586        assert_eq!(builder.pending_operations(), 2); // 12 % 5
587    }
588
589    #[test]
590    fn test_mixed_operations() {
591        let config = BatchBuilderConfig {
592            group_by_type: true,
593            auto_flush: false,
594            ..Default::default()
595        };
596
597        let mut builder = BatchBuilder::new(config);
598
599        // Add different operation types
600        builder
601            .insert(create_test_triple(1))
602            .expect("builder insert should succeed");
603        builder
604            .remove(create_test_triple(2))
605            .expect("builder remove should succeed");
606        builder
607            .query(None, None, None)
608            .expect("query should succeed");
609
610        let batches = builder.flush().expect("flush should succeed");
611
612        // Should have 3 batches (one per type)
613        assert_eq!(batches.len(), 3);
614    }
615
616    #[test]
617    fn test_memory_limits() {
618        let config = BatchBuilderConfig {
619            max_memory_usage: 1000, // Very small limit
620            auto_flush: true,
621            ..Default::default()
622        };
623
624        let mut builder = BatchBuilder::new(config);
625
626        // Add operations until memory limit
627        let mut added = 0;
628        for i in 0..100 {
629            builder
630                .insert(create_test_triple(i))
631                .expect("builder insert should succeed");
632            added += 1;
633            if builder.pending_operations() == 0 {
634                // Auto-flushed due to memory
635                break;
636            }
637        }
638
639        // Should have flushed before adding all 100
640        assert!(added < 100);
641        assert_eq!(builder.stats().batches_created, 1);
642    }
643}