1use crate::algebra::{Binding, Solution, Term, Variable};
7use anyhow::{anyhow, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fs::{remove_file, File};
11use std::io::{BufReader, BufWriter, Read, Write};
12use std::path::PathBuf;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, RwLock};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18struct SerializableSolution(Vec<SerializableBinding>);
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22struct SerializableBinding(Vec<(String, Term)>);
23
24impl From<&Solution> for SerializableSolution {
25 fn from(solution: &Solution) -> Self {
26 SerializableSolution(solution.iter().map(SerializableBinding::from).collect())
27 }
28}
29
30impl From<SerializableSolution> for Solution {
31 fn from(ser: SerializableSolution) -> Self {
32 ser.0
33 .into_iter()
34 .map(|ser_binding| {
35 let mut binding = Binding::new();
36 for (var_name, term) in ser_binding.0 {
37 if let Ok(var) = Variable::new(&var_name) {
38 binding.insert(var, term);
39 }
40 }
41 binding
42 })
43 .collect()
44 }
45}
46
47impl From<&Binding> for SerializableBinding {
48 fn from(binding: &Binding) -> Self {
49 SerializableBinding(
50 binding
51 .iter()
52 .map(|(var, term)| (var.to_string(), term.clone()))
53 .collect(),
54 )
55 }
56}
57
58pub struct SpillManager {
60 spill_dir: PathBuf,
61 config: SpillConfig,
62 current_memory_bytes: Arc<AtomicU64>,
63 active_spills: Arc<RwLock<HashMap<SpillId, SpillFile>>>,
64}
65
66#[derive(Debug, Clone)]
68pub struct SpillConfig {
69 pub spill_threshold_percent: f64, pub max_memory_mb: usize, pub spill_dir: PathBuf, pub compression: bool, }
74
75impl Default for SpillConfig {
76 fn default() -> Self {
77 Self {
78 spill_threshold_percent: 0.8,
79 max_memory_mb: 2048,
80 spill_dir: std::env::temp_dir().join("oxirs_spill"),
81 compression: true,
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct SpillFile {
89 pub id: SpillId,
90 pub path: PathBuf,
91 pub size: usize,
92 pub num_rows: usize,
93 pub compressed: bool,
94}
95
96#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
98pub struct SpillId(u64);
99
100impl SpillId {
101 fn new() -> Self {
102 static COUNTER: AtomicU64 = AtomicU64::new(1);
103 SpillId(COUNTER.fetch_add(1, Ordering::Relaxed))
104 }
105}
106
107impl SpillManager {
108 pub fn new(config: SpillConfig) -> Result<Self> {
110 let unique_id = SpillId::new();
113 let spill_dir = config.spill_dir.join(format!("instance_{}", unique_id.0));
114
115 std::fs::create_dir_all(&spill_dir)?;
117
118 Ok(Self {
119 spill_dir,
120 config,
121 current_memory_bytes: Arc::new(AtomicU64::new(0)),
122 active_spills: Arc::new(RwLock::new(HashMap::new())),
123 })
124 }
125
126 pub fn should_spill(&self) -> bool {
128 let current_memory_mb = self.current_memory_usage_mb();
129 let threshold_mb =
130 (self.config.max_memory_mb as f64 * self.config.spill_threshold_percent) as usize;
131
132 current_memory_mb > threshold_mb
133 }
134
135 pub fn current_memory_usage_mb(&self) -> usize {
137 (self.current_memory_bytes.load(Ordering::Relaxed) / (1024 * 1024)) as usize
138 }
139
140 pub fn spill(&mut self, data: &Solution) -> Result<SpillId> {
142 if data.is_empty() {
143 return Err(anyhow!("Cannot spill empty solution"));
144 }
145
146 let spill_id = SpillId::new();
147 let path = self.spill_dir.join(format!("spill_{}.bin", spill_id.0));
148
149 let spill = if self.config.compression {
150 let compressed = self.compress_solution(data)?;
152
153 {
154 let file = File::create(&path)?;
155 let mut writer = BufWriter::new(file);
156 writer.write_all(&compressed)?;
157 writer.flush()?;
158 drop(writer);
160 }
161
162 let size = compressed.len();
163 let spill = SpillFile {
164 id: spill_id,
165 path: path.clone(),
166 size,
167 num_rows: data.len(),
168 compressed: true,
169 };
170
171 tracing::info!(
172 "Spilled {} rows to {} (compressed, {} bytes)",
173 data.len(),
174 path.display(),
175 size
176 );
177
178 spill
179 } else {
180 let serializable = SerializableSolution::from(data);
182
183 {
184 let file = File::create(&path)?;
185 let mut writer = BufWriter::new(file);
186 serde_json::to_writer(&mut writer, &serializable)?;
187 writer.flush()?;
188 drop(writer);
190 }
191
192 let size = std::fs::metadata(&path)?.len() as usize;
193 let spill = SpillFile {
194 id: spill_id,
195 path: path.clone(),
196 size,
197 num_rows: data.len(),
198 compressed: false,
199 };
200
201 tracing::info!(
202 "Spilled {} rows to {} ({} bytes)",
203 data.len(),
204 path.display(),
205 size
206 );
207
208 spill
209 };
210
211 self.active_spills
212 .write()
213 .map_err(|_| anyhow!("Failed to acquire write lock"))?
214 .insert(spill_id, spill);
215
216 Ok(spill_id)
217 }
218
219 pub fn spill_bindings(&mut self, data: &[Binding]) -> Result<SpillId> {
221 let solution: Solution = data.to_vec();
222 self.spill(&solution)
223 }
224
225 pub fn read_spill(&self, spill_id: SpillId) -> Result<Solution> {
227 let spills = self
228 .active_spills
229 .read()
230 .map_err(|_| anyhow!("Failed to acquire read lock"))?;
231 let spill = spills
232 .get(&spill_id)
233 .ok_or_else(|| anyhow!("Spill {} not found", spill_id.0))?;
234
235 let file = File::open(&spill.path)?;
236 let mut reader = BufReader::new(file);
237
238 let data = if spill.compressed {
239 let mut compressed = Vec::new();
240 reader.read_to_end(&mut compressed)?;
241 self.decompress_solution(&compressed)?
242 } else {
243 let serializable: SerializableSolution = serde_json::from_reader(&mut reader)?;
244 Solution::from(serializable)
245 };
246
247 tracing::debug!("Read {} rows from spill {}", data.len(), spill_id.0);
248 Ok(data)
249 }
250
251 pub fn read_spill_streaming(&self, spill_id: SpillId) -> Result<SpillIterator> {
253 let spills = self
254 .active_spills
255 .read()
256 .map_err(|_| anyhow!("Failed to acquire read lock"))?;
257 let spill = spills
258 .get(&spill_id)
259 .ok_or_else(|| anyhow!("Spill {} not found", spill_id.0))?
260 .clone();
261
262 SpillIterator::new(spill)
263 }
264
265 pub fn cleanup(&mut self, spill_id: SpillId) -> Result<()> {
267 let mut spills = self
268 .active_spills
269 .write()
270 .map_err(|_| anyhow!("Failed to acquire write lock"))?;
271 if let Some(spill) = spills.remove(&spill_id) {
272 if spill.path.exists() {
273 remove_file(&spill.path)?;
274 }
275 tracing::debug!("Cleaned up spill {}", spill_id.0);
276 }
277 Ok(())
278 }
279
280 pub fn cleanup_all(&mut self) -> Result<()> {
282 let mut spills = self
283 .active_spills
284 .write()
285 .map_err(|_| anyhow!("Failed to acquire write lock"))?;
286
287 for (_, spill) in spills.drain() {
288 if spill.path.exists() {
289 if let Err(e) = remove_file(&spill.path) {
290 tracing::warn!(
291 "Failed to remove spill file {}: {}",
292 spill.path.display(),
293 e
294 );
295 }
296 }
297 }
298
299 tracing::info!("Cleaned up all spill files");
300 Ok(())
301 }
302
303 pub fn statistics(&self) -> Result<SpillStatistics> {
305 let spills = self
306 .active_spills
307 .read()
308 .map_err(|_| anyhow!("Failed to acquire read lock"))?;
309
310 let total_size = spills.values().map(|s| s.size).sum();
311 let total_rows = spills.values().map(|s| s.num_rows).sum();
312
313 Ok(SpillStatistics {
314 num_spills: spills.len(),
315 total_size_bytes: total_size,
316 total_rows,
317 average_compression_ratio: self.calculate_average_compression_ratio(&spills),
318 })
319 }
320
321 fn calculate_average_compression_ratio(&self, spills: &HashMap<SpillId, SpillFile>) -> f64 {
322 let compressed_spills: Vec<_> = spills.values().filter(|s| s.compressed).collect();
323
324 if compressed_spills.is_empty() {
325 return 1.0;
326 }
327
328 0.2
330 }
331
332 fn compress_solution(&self, data: &Solution) -> Result<Vec<u8>> {
334 let serializable = SerializableSolution::from(data);
337 let json_data = serde_json::to_vec(&serializable)?;
338 let compressed = oxiarc_deflate::gzip_compress(&json_data, 6)?;
339 Ok(compressed)
340 }
341
342 fn decompress_solution(&self, compressed: &[u8]) -> Result<Solution> {
344 let json_data = oxiarc_deflate::gzip_decompress(compressed)?;
346 let serializable: SerializableSolution = serde_json::from_slice(&json_data)?;
347 Ok(Solution::from(serializable))
348 }
349
350 pub fn num_active_spills(&self) -> usize {
352 self.active_spills.read().map(|s| s.len()).unwrap_or(0)
353 }
354}
355
356impl Drop for SpillManager {
357 fn drop(&mut self) {
358 if let Err(e) = self.cleanup_all() {
360 tracing::warn!("Failed to clean up spill files in Drop: {}", e);
361 }
362 }
363}
364
365#[derive(Debug, Clone)]
367pub struct SpillStatistics {
368 pub num_spills: usize,
369 pub total_size_bytes: usize,
370 pub total_rows: usize,
371 pub average_compression_ratio: f64,
372}
373
374pub struct SpillIterator {
376 spill: SpillFile,
377 buffer: Option<Solution>,
378 current_index: usize,
379}
380
381impl SpillIterator {
382 fn new(spill: SpillFile) -> Result<Self> {
383 Ok(Self {
384 spill,
385 buffer: None,
386 current_index: 0,
387 })
388 }
389
390 fn load_buffer(&mut self) -> Result<()> {
391 let file = File::open(&self.spill.path)?;
392 let mut reader = BufReader::new(file);
393
394 let data = if self.spill.compressed {
395 let mut compressed = Vec::new();
397 reader.read_to_end(&mut compressed)?;
398 let json_data = oxiarc_deflate::gzip_decompress(&compressed)?;
399 let serializable: SerializableSolution = serde_json::from_slice(&json_data)?;
400 Solution::from(serializable)
401 } else {
402 let serializable: SerializableSolution = serde_json::from_reader(&mut reader)?;
403 Solution::from(serializable)
404 };
405
406 self.buffer = Some(data);
407 self.current_index = 0;
408 Ok(())
409 }
410}
411
412impl Iterator for SpillIterator {
413 type Item = Result<Binding>;
414
415 fn next(&mut self) -> Option<Self::Item> {
416 if self.buffer.is_none() {
418 if let Err(e) = self.load_buffer() {
419 return Some(Err(e));
420 }
421 }
422
423 if let Some(ref buffer) = self.buffer {
424 if self.current_index < buffer.len() {
425 let binding = buffer[self.current_index].clone();
426 self.current_index += 1;
427 Some(Ok(binding))
428 } else {
429 None
430 }
431 } else {
432 None
433 }
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::algebra::Variable;
441
442 fn create_test_solution(size: usize) -> Solution {
443 let mut solution = Solution::new();
444 for i in 0..size {
445 let mut binding = Binding::new();
446 binding.insert(
447 Variable::new(format!("x{}", i)).expect("valid variable name"),
448 crate::algebra::Term::Iri(
449 oxirs_core::model::NamedNode::new(format!("http://example.org/item{}", i))
450 .expect("valid IRI"),
451 ),
452 );
453 solution.push(binding);
454 }
455 solution
456 }
457
458 #[test]
459 fn test_spill_manager_creation() {
460 let config = SpillConfig::default();
461 let manager = SpillManager::new(config);
462 assert!(manager.is_ok());
463 }
464
465 #[test]
466 fn test_spill_and_read() {
467 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
468 let config = SpillConfig {
469 spill_dir: temp_dir.path().to_path_buf(),
470 ..SpillConfig::default()
471 };
472 let mut manager = SpillManager::new(config).expect("Failed to create manager");
473
474 let test_data = create_test_solution(100);
475 let spill_id = manager.spill(&test_data).expect("Failed to spill");
476
477 let read_data = manager.read_spill(spill_id).expect("Failed to read spill");
478 assert_eq!(test_data.len(), read_data.len());
479
480 manager.cleanup(spill_id).expect("Failed to cleanup");
481 }
482
483 #[test]
484 fn test_spill_statistics() {
485 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
486 let config = SpillConfig {
487 spill_dir: temp_dir.path().to_path_buf(),
488 ..SpillConfig::default()
489 };
490 let mut manager = SpillManager::new(config).expect("Failed to create manager");
491
492 let test_data = create_test_solution(50);
493 let _spill_id = manager.spill(&test_data).expect("Failed to spill");
494
495 let stats = manager.statistics().expect("Failed to get statistics");
496 assert_eq!(stats.num_spills, 1);
497 assert_eq!(stats.total_rows, 50);
498
499 manager.cleanup_all().expect("Failed to cleanup");
500 }
501}