1use 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
13type TransformFn = Arc<dyn Fn(&Triple) -> Option<Triple> + Send + Sync>;
15
16type FlushCallback = Arc<Mutex<Option<Box<dyn Fn(Vec<BatchOperation>) + Send + Sync>>>>;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CoalescingStrategy {
22 None,
24 Deduplicate,
26 Merge,
28 OptimizeOrder,
30}
31
32#[derive(Debug, Clone)]
34pub struct BatchBuilderConfig {
35 pub max_batch_size: usize,
37 pub max_memory_usage: usize,
39 pub coalescing_strategy: CoalescingStrategy,
41 pub auto_flush: bool,
43 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) .unwrap_or(8 * 1024 * 1024 * 1024); BatchBuilderConfig {
54 max_batch_size: 10000,
55 max_memory_usage: (total_memory as usize) / 10, coalescing_strategy: CoalescingStrategy::Deduplicate,
57 auto_flush: true,
58 group_by_type: true,
59 }
60 }
61}
62
63impl BatchBuilderConfig {
64 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 (50000, (info.total * 1024 / 8) as usize) } else if total_mb > 8192 {
77 (20000, (info.total * 1024 / 10) as usize) } else {
80 (5000, (info.total * 1024 / 20) as usize) }
82 } else {
83 (10000, 1024 * 1024 * 1024) };
85
86 BatchBuilderConfig {
87 max_batch_size: max_batch_size * num_cpus / 4, max_memory_usage,
89 coalescing_strategy: CoalescingStrategy::Merge,
90 auto_flush: true,
91 group_by_type: true,
92 }
93 }
94}
95
96#[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
106pub struct BatchBuilder {
108 config: BatchBuilderConfig,
109 insert_buffer: Vec<Triple>,
111 insert_set: HashSet<Triple>,
112 remove_buffer: Vec<Triple>,
114 remove_set: HashSet<Triple>,
115 query_buffer: Vec<(Option<Subject>, Option<Predicate>, Option<Object>)>,
117 transform_buffer: Vec<TransformFn>,
119 estimated_memory: usize,
121 stats: BatchBuilderStats,
123 flush_callback: FlushCallback,
125}
126
127impl BatchBuilder {
128 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 pub fn auto() -> Self {
146 Self::new(BatchBuilderConfig::auto())
147 }
148
149 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 pub fn insert(&mut self, triple: Triple) -> Result<(), OxirsError> {
159 self.stats.total_operations += 1;
160
161 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 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 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 pub fn remove(&mut self, triple: Triple) -> Result<(), OxirsError> {
198 self.stats.total_operations += 1;
199
200 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 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; self.check_flush()?;
238 Ok(())
239 }
240
241 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; self.check_flush()?;
251 Ok(())
252 }
253
254 pub fn stats(&self) -> &BatchBuilderStats {
256 &self.stats
257 }
258
259 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 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 fn estimate_triple_size(&self, triple: &Triple) -> usize {
282 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 }
294
295 fn estimate_object_size(&self, _object: &Object) -> usize {
296 150 }
298
299 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 if self.config.group_by_type {
309 if self.config.coalescing_strategy == CoalescingStrategy::OptimizeOrder {
311 self.optimize_operation_order();
312 }
313
314 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 operations = self.create_mixed_batches();
333 }
334
335 self.stats.batches_created += operations.len();
337 self.stats.estimated_memory_usage = self.estimated_memory;
338
339 self.clear();
341
342 if let Some(callback) = &*self.flush_callback.lock() {
344 callback(operations.clone());
345 }
346
347 Ok(operations)
348 }
349
350 fn apply_merge_coalescing(&mut self) {
352 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 let insert_set = &self.insert_set;
364 self.remove_buffer
365 .retain(|triple| !insert_set.contains(triple));
366 }
367 }
368 }
369
370 fn optimize_operation_order(&mut self) {
372 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 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 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 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 fn create_transform_batches(&mut self) -> Vec<BatchOperation> {
430 self.transform_buffer
431 .drain(..)
432 .map(BatchOperation::Transform)
433 .collect()
434 }
435
436 fn create_mixed_batches(&mut self) -> Vec<BatchOperation> {
438 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 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
462impl 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 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 let batches = builder.flush().expect("flush should succeed");
512 assert_eq!(batches.len(), 3); 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 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 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 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 for i in 0..12 {
579 builder
580 .insert(create_test_triple(i))
581 .expect("builder insert should succeed");
582 }
583
584 assert_eq!(flushed_batches.lock().len(), 2);
586 assert_eq!(builder.pending_operations(), 2); }
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 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 assert_eq!(batches.len(), 3);
614 }
615
616 #[test]
617 fn test_memory_limits() {
618 let config = BatchBuilderConfig {
619 max_memory_usage: 1000, auto_flush: true,
621 ..Default::default()
622 };
623
624 let mut builder = BatchBuilder::new(config);
625
626 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 break;
636 }
637 }
638
639 assert!(added < 100);
641 assert_eq!(builder.stats().batches_created, 1);
642 }
643}