1use crate::spill_file::{SpillFile, SpillWriter, TempFileFactory};
21use bytes::Bytes;
22use datafusion_common::human_readable_size;
23use datafusion_common::{DataFusionError, Result, config_err, resources_datafusion_err};
24#[cfg(not(target_arch = "wasm32"))]
25use futures::StreamExt;
26use log::debug;
27use parking_lot::Mutex;
28use rand::{Rng, rng};
29use std::fmt::Debug;
30use std::path::{Path, PathBuf};
31use std::sync::Arc;
32use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
33use tempfile::{Builder, NamedTempFile, TempDir};
34pub const DEFAULT_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; pub const DEFAULT_MAX_SPILL_MERGE_FAN_IN: usize = 0;
36
37#[derive(Clone)]
39pub struct DiskManagerBuilder {
40 mode: DiskManagerMode,
42 max_temp_directory_size: u64,
45 max_spill_merge_fan_in: usize,
48}
49impl Debug for DiskManagerBuilder {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("DiskManagerBuilder")
52 .field("mode", &self.mode)
53 .field("max_temp_directory_size", &self.max_temp_directory_size)
54 .finish()
55 }
56}
57impl Default for DiskManagerBuilder {
58 fn default() -> Self {
59 Self {
60 mode: DiskManagerMode::OsTmpDirectory,
61 max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE,
62 max_spill_merge_fan_in: DEFAULT_MAX_SPILL_MERGE_FAN_IN,
63 }
64 }
65}
66
67impl DiskManagerBuilder {
68 pub fn set_mode(&mut self, mode: DiskManagerMode) {
69 self.mode = mode;
70 }
71
72 pub fn with_mode(mut self, mode: DiskManagerMode) -> Self {
73 self.set_mode(mode);
74 self
75 }
76
77 pub fn set_temp_file_factory(&mut self, temp_file_factory: Arc<dyn TempFileFactory>) {
83 self.mode = DiskManagerMode::Custom(temp_file_factory);
84 }
85
86 pub fn with_temp_file_factory(
90 mut self,
91 temp_file_factory: Arc<dyn TempFileFactory>,
92 ) -> Self {
93 self.set_temp_file_factory(temp_file_factory);
94 self
95 }
96
97 pub fn set_max_temp_directory_size(&mut self, value: u64) {
98 self.max_temp_directory_size = value;
99 }
100
101 pub fn with_max_temp_directory_size(mut self, value: u64) -> Self {
102 self.set_max_temp_directory_size(value);
103 self
104 }
105
106 pub fn set_max_spill_merge_fan_in(&mut self, value: usize) {
107 self.max_spill_merge_fan_in = value;
108 }
109
110 pub fn with_max_spill_merge_fan_in(mut self, value: usize) -> Self {
111 self.set_max_spill_merge_fan_in(value);
112 self
113 }
114
115 pub fn build(self) -> Result<DiskManager> {
117 match self.mode {
118 DiskManagerMode::OsTmpDirectory => Ok(DiskManager {
119 local_dirs: Mutex::new(Some(vec![])),
120 max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
121 max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
122 used_disk_space: Arc::new(AtomicU64::new(0)),
123 active_files_count: Arc::new(AtomicUsize::new(0)),
124 factory: None,
125 }),
126 DiskManagerMode::Directories(conf_dirs) => {
127 let local_dirs = create_local_dirs(&conf_dirs)?;
128 debug!(
129 "Created local dirs {local_dirs:?} as DataFusion working directory"
130 );
131 Ok(DiskManager {
132 local_dirs: Mutex::new(Some(local_dirs)),
133 max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
134 max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
135 used_disk_space: Arc::new(AtomicU64::new(0)),
136 active_files_count: Arc::new(AtomicUsize::new(0)),
137 factory: None,
138 })
139 }
140 DiskManagerMode::Disabled => Ok(DiskManager {
141 local_dirs: Mutex::new(None),
142 max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
143 max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
144 used_disk_space: Arc::new(AtomicU64::new(0)),
145 active_files_count: Arc::new(AtomicUsize::new(0)),
146 factory: None,
147 }),
148 DiskManagerMode::Custom(factory) => Ok(DiskManager {
149 local_dirs: Mutex::new(None),
150 max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
151 max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
152 used_disk_space: Arc::new(AtomicU64::new(0)),
153 active_files_count: Arc::new(AtomicUsize::new(0)),
154 factory: Some(factory),
155 }),
156 }
157 }
158}
159
160#[derive(Clone, Default)]
161pub enum DiskManagerMode {
162 #[default]
165 OsTmpDirectory,
166
167 Directories(Vec<PathBuf>),
171
172 Custom(Arc<dyn TempFileFactory>),
174
175 Disabled,
177}
178
179impl Debug for DiskManagerMode {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 match self {
182 Self::OsTmpDirectory => write!(f, "OsTmpDirectory"),
183 Self::Directories(dirs) => f.debug_tuple("Directories").field(dirs).finish(),
184 Self::Disabled => write!(f, "Disabled"),
185 Self::Custom(_) => write!(f, "Custom(Arc<dyn TempFileFactory>)"),
186 }
187 }
188}
189
190pub struct DiskManager {
193 local_dirs: Mutex<Option<Vec<Arc<TempDir>>>>,
198 max_temp_directory_size: AtomicU64,
202 max_spill_merge_fan_in: AtomicUsize,
205 used_disk_space: Arc<AtomicU64>,
208 active_files_count: Arc<AtomicUsize>,
210 factory: Option<Arc<dyn TempFileFactory>>,
212}
213impl Debug for DiskManager {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("DiskManager")
216 .field("local_dirs", &self.local_dirs)
217 .field("max_temp_directory_size", &self.max_temp_directory_size)
218 .field("used_disk_space", &self.used_disk_space)
219 .field("active_files_count", &self.active_files_count)
220 .field("factory", &self.factory.is_some())
221 .finish()
222 }
223}
224#[derive(Debug, Clone, Copy)]
226pub struct SpillingProgress {
227 pub current_bytes: u64,
229 pub active_files_count: usize,
231}
232
233impl DiskManager {
234 pub fn builder() -> DiskManagerBuilder {
236 DiskManagerBuilder::default()
237 }
238
239 pub fn set_max_temp_directory_size(
247 &self,
248 max_temp_directory_size: u64,
249 ) -> Result<()> {
250 if self.local_dirs.lock().is_none()
253 && max_temp_directory_size != 0
254 && self.factory.is_none()
255 {
256 return config_err!(
257 "Cannot set max temp directory size for a disk manager that spilling is disabled"
258 );
259 }
260
261 self.max_temp_directory_size
262 .store(max_temp_directory_size, Ordering::Relaxed);
263 Ok(())
264 }
265
266 #[deprecated(
267 since = "54.0.0",
268 note = "Use `set_max_temp_directory_size` directly, it now takes &self"
269 )]
270 pub fn set_arc_max_temp_directory_size(
271 this: &Arc<Self>,
272 max_temp_directory_size: u64,
273 ) -> Result<()> {
274 this.set_max_temp_directory_size(max_temp_directory_size)
275 }
276
277 pub fn with_max_temp_directory_size(
278 self,
279 max_temp_directory_size: u64,
280 ) -> Result<Self> {
281 self.set_max_temp_directory_size(max_temp_directory_size)?;
282 Ok(self)
283 }
284
285 pub fn used_disk_space(&self) -> u64 {
286 self.used_disk_space.load(Ordering::Relaxed)
287 }
288
289 pub fn max_temp_directory_size(&self) -> u64 {
291 self.max_temp_directory_size.load(Ordering::Relaxed)
292 }
293
294 pub fn set_max_spill_merge_fan_in(&self, max_spill_merge_fan_in: usize) {
299 self.max_spill_merge_fan_in
300 .store(max_spill_merge_fan_in, Ordering::Relaxed);
301 }
302
303 pub fn max_spill_merge_fan_in(&self) -> usize {
307 self.max_spill_merge_fan_in.load(Ordering::Relaxed)
308 }
309
310 pub fn spilling_progress(&self) -> SpillingProgress {
312 SpillingProgress {
313 current_bytes: self.used_disk_space.load(Ordering::Relaxed),
314 active_files_count: self.active_files_count.load(Ordering::Relaxed),
315 }
316 }
317
318 pub fn temp_dir_paths(&self) -> Vec<PathBuf> {
320 self.local_dirs
321 .lock()
322 .as_ref()
323 .map(|dirs| {
324 dirs.iter()
325 .map(|temp_dir| temp_dir.path().to_path_buf())
326 .collect()
327 })
328 .unwrap_or_default()
329 }
330
331 pub fn tmp_files_enabled(&self) -> bool {
335 self.factory.is_some() || self.local_dirs.lock().is_some()
336 }
337
338 pub fn create_tmp_file(
343 self: &Arc<Self>,
344 request_description: &str,
345 ) -> Result<Arc<dyn SpillFile>> {
346 if let Some(factory) = &self.factory {
348 return factory.create_temp_file(request_description);
349 }
350 let mut guard = self.local_dirs.lock();
351 let local_dirs = guard.as_mut().ok_or_else(|| {
352 resources_datafusion_err!(
353 "Memory Exhausted while {request_description} (DiskManager is disabled)"
354 )
355 })?;
356
357 if local_dirs.is_empty() {
359 let tempdir = tempfile::tempdir().map_err(DataFusionError::IoError)?;
360
361 debug!(
362 "Created directory '{:?}' as DataFusion tempfile directory for {}",
363 tempdir.path().to_string_lossy(),
364 request_description,
365 );
366
367 local_dirs.push(Arc::new(tempdir));
368 }
369
370 let dir_index = rng().random_range(0..local_dirs.len());
371 self.active_files_count.fetch_add(1, Ordering::Relaxed);
372 Ok(Arc::new(RefCountedTempFile {
373 parent_temp_dir: Arc::clone(&local_dirs[dir_index]),
374 tempfile: Arc::new(
375 Builder::new()
376 .tempfile_in(local_dirs[dir_index].as_ref())
377 .map_err(DataFusionError::IoError)?,
378 ),
379 current_file_disk_usage: Arc::new(AtomicU64::new(0)),
380 disk_manager: Arc::clone(self),
381 }))
382 }
383}
384
385#[derive(Debug)]
397pub struct RefCountedTempFile {
398 parent_temp_dir: Arc<TempDir>,
401 tempfile: Arc<NamedTempFile>,
403 current_file_disk_usage: Arc<AtomicU64>,
408 disk_manager: Arc<DiskManager>,
410}
411
412impl Clone for RefCountedTempFile {
413 fn clone(&self) -> Self {
414 Self {
415 parent_temp_dir: Arc::clone(&self.parent_temp_dir),
416 tempfile: Arc::clone(&self.tempfile),
417 current_file_disk_usage: Arc::clone(&self.current_file_disk_usage),
418 disk_manager: Arc::clone(&self.disk_manager),
419 }
420 }
421}
422
423impl RefCountedTempFile {
424 pub fn path(&self) -> &Path {
425 self.tempfile.path()
426 }
427
428 pub fn inner(&self) -> &NamedTempFile {
429 self.tempfile.as_ref()
430 }
431
432 fn current_disk_usage(&self) -> u64 {
433 self.current_file_disk_usage.load(Ordering::Relaxed)
434 }
435}
436
437impl Drop for RefCountedTempFile {
439 fn drop(&mut self) {
440 if Arc::strong_count(&self.tempfile) == 1 {
444 let current_usage = self.current_file_disk_usage.load(Ordering::Relaxed);
445 self.disk_manager
446 .used_disk_space
447 .fetch_sub(current_usage, Ordering::Relaxed);
448 self.disk_manager
449 .active_files_count
450 .fetch_sub(1, Ordering::Relaxed);
451 }
452 }
453}
454
455fn create_local_dirs(local_dirs: &[PathBuf]) -> Result<Vec<Arc<TempDir>>> {
457 local_dirs
458 .iter()
459 .map(|root| {
460 if !Path::new(root).exists() {
461 std::fs::create_dir(root)?;
462 }
463 Builder::new()
464 .prefix("datafusion-")
465 .tempdir_in(root)
466 .map_err(DataFusionError::IoError)
467 })
468 .map(|result| result.map(Arc::new))
469 .collect()
470}
471
472pub struct FileSpillWriter {
473 file: std::fs::File,
474 disk_manager: Arc<DiskManager>,
475 current_file_disk_usage: Arc<AtomicU64>,
476}
477
478impl std::io::Write for FileSpillWriter {
479 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
480 let len = buf.len() as u64;
481 if len == 0 {
482 return Ok(0);
483 }
484
485 let new_global = self
486 .disk_manager
487 .used_disk_space
488 .fetch_add(len, Ordering::Relaxed)
489 + len;
490
491 let limit = self.disk_manager.max_temp_directory_size();
492
493 if new_global > limit {
494 self.disk_manager
495 .used_disk_space
496 .fetch_sub(len, Ordering::Relaxed);
497
498 return Err(std::io::Error::other(format!(
499 "The used disk space during the spilling process has exceeded the allowable limit of {}. \
500 Please try increasing the config: `datafusion.runtime.max_temp_directory_size`.",
501 human_readable_size(limit as usize)
502 )));
503 }
504
505 self.file.write_all(buf).map_err(DataFusionError::IoError)?;
506
507 self.current_file_disk_usage
508 .fetch_add(len, Ordering::Relaxed);
509
510 Ok(buf.len())
511 }
512
513 fn flush(&mut self) -> std::io::Result<()> {
514 self.file.flush()
515 }
516}
517
518impl SpillWriter for FileSpillWriter {
519 fn finish(&mut self) -> Result<()> {
520 Ok(())
522 }
523}
524
525impl SpillFile for RefCountedTempFile {
526 fn path(&self) -> Option<&Path> {
527 Some(self.tempfile.path())
528 }
529
530 fn size(&self) -> Option<u64> {
531 Some(self.current_disk_usage())
532 }
533 #[cfg(not(target_arch = "wasm32"))]
534 fn read_stream(
535 &self,
536 ) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<Bytes>> + Send>>>
537 {
538 let path = self.path().to_owned();
539
540 let stream =
541 futures::stream::once(async move {
542 tokio::fs::File::open(&path)
543 .await
544 .map_err(DataFusionError::IoError)
545 })
546 .flat_map(
547 |open_result| -> std::pin::Pin<
548 Box<dyn futures::Stream<Item = Result<Bytes>> + Send>,
549 > {
550 match open_result {
551 Ok(file) => Box::pin(
552 tokio_util::io::ReaderStream::with_capacity(file, 128 * 1024)
555 .map(|r| r.map_err(DataFusionError::IoError)),
556 ),
557 Err(e) => Box::pin(futures::stream::once(async move { Err(e) })),
558 }
559 },
560 );
561
562 Ok(Box::pin(stream))
563 }
564
565 #[cfg(target_arch = "wasm32")]
566 fn read_stream(
567 &self,
568 ) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<Bytes>> + Send>>>
569 {
570 datafusion_common::exec_err!(
571 "Default OS file spilling is not supported on WASM. Configure DiskManager with a Custom TempFileFactory."
572 )
573 }
574
575 fn open_writer(&self) -> Result<Box<dyn SpillWriter>> {
576 let file = self
577 .tempfile
578 .as_file()
579 .try_clone()
580 .map_err(DataFusionError::IoError)?;
581 Ok(Box::new(FileSpillWriter {
582 file,
583 disk_manager: Arc::clone(&self.disk_manager),
584 current_file_disk_usage: Arc::clone(&self.current_file_disk_usage),
585 }))
586 }
587}
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn lazy_temp_dir_creation() -> Result<()> {
594 let dm = Arc::new(DiskManagerBuilder::default().build()?);
596
597 assert_eq!(0, local_dir_snapshot(&dm).len());
598
599 let actual = dm.create_tmp_file("Testing")?;
601
602 assert_eq!(1, local_dir_snapshot(&dm).len());
604
605 let local_dirs = local_dir_snapshot(&dm);
607 assert_path_in_dirs(
608 actual.path().unwrap(),
609 local_dirs.iter().map(|p| p.as_path()),
610 );
611
612 Ok(())
613 }
614
615 fn local_dir_snapshot(dm: &DiskManager) -> Vec<PathBuf> {
616 dm.local_dirs
617 .lock()
618 .iter()
619 .flatten()
620 .map(|p| p.path().into())
621 .collect()
622 }
623
624 #[test]
625 fn file_in_right_dir() -> Result<()> {
626 let local_dir1 = TempDir::new()?;
627 let local_dir2 = TempDir::new()?;
628 let local_dir3 = TempDir::new()?;
629 let local_dirs = vec![local_dir1.path(), local_dir2.path(), local_dir3.path()];
630 let dm = Arc::new(
631 DiskManagerBuilder::default()
632 .with_mode(DiskManagerMode::Directories(
633 local_dirs.iter().map(|p| p.into()).collect(),
634 ))
635 .build()?,
636 );
637
638 assert!(dm.tmp_files_enabled());
639 let actual = dm.create_tmp_file("Testing")?;
640
641 assert_path_in_dirs(actual.path().unwrap(), local_dirs.into_iter());
643
644 Ok(())
645 }
646
647 #[test]
648 fn test_disabled_disk_manager() {
649 let manager = Arc::new(
650 DiskManagerBuilder::default()
651 .with_mode(DiskManagerMode::Disabled)
652 .build()
653 .unwrap(),
654 );
655 assert!(!manager.tmp_files_enabled());
656 match manager.create_tmp_file("Testing") {
657 Err(e) => {
658 assert_eq!(
659 e.strip_backtrace(),
660 "Resources exhausted: Memory Exhausted while Testing (DiskManager is disabled)"
661 );
662 }
663 Ok(_) => {
664 panic!("Expected DiskManager to fail creating a file when disabled!")
665 }
666 }
667 }
668
669 #[test]
670 fn test_disk_manager_create_spill_folder() {
671 let dir = TempDir::new().unwrap();
672 DiskManagerBuilder::default()
673 .with_mode(DiskManagerMode::Directories(vec![dir.path().to_path_buf()]))
674 .build()
675 .unwrap();
676 }
677
678 fn assert_path_in_dirs<'a>(
680 file_path: &'a Path,
681 dirs: impl Iterator<Item = &'a Path>,
682 ) {
683 let dirs: Vec<&Path> = dirs.collect();
684
685 let found = dirs.iter().any(|dir_path| {
686 file_path
687 .ancestors()
688 .any(|candidate_path| *dir_path == candidate_path)
689 });
690
691 assert!(found, "Can't find {file_path:?} in dirs: {dirs:?}");
692 }
693
694 #[test]
695 fn test_temp_file_still_alive_after_disk_manager_dropped() -> Result<()> {
696 let dm = Arc::new(DiskManagerBuilder::default().build()?);
698 let temp_file = dm.create_tmp_file("Testing")?;
699 let temp_file_path = temp_file.path().unwrap().to_owned();
700 assert!(temp_file_path.exists());
701
702 drop(dm);
703 assert!(temp_file_path.exists());
704
705 drop(temp_file);
706 assert!(!temp_file_path.exists());
707
708 let local_dir1 = TempDir::new()?;
710 let local_dir2 = TempDir::new()?;
711 let local_dir3 = TempDir::new()?;
712 let local_dirs = [local_dir1.path(), local_dir2.path(), local_dir3.path()];
713 let dm = Arc::new(
714 DiskManagerBuilder::default()
715 .with_mode(DiskManagerMode::Directories(
716 local_dirs.iter().map(|p| p.into()).collect(),
717 ))
718 .build()?,
719 );
720 let temp_file = dm.create_tmp_file("Testing")?;
721 let temp_file_path = temp_file.path().unwrap().to_owned();
722 assert!(temp_file_path.exists());
723
724 drop(dm);
725 assert!(temp_file_path.exists());
726
727 drop(temp_file);
728 assert!(!temp_file_path.exists());
729
730 Ok(())
731 }
732
733 #[test]
734 fn test_disk_usage_basic() -> Result<()> {
735 let dm = Arc::new(DiskManagerBuilder::default().build()?);
736 let temp_file = dm.create_tmp_file("Testing")?;
737 let mut writer = temp_file.open_writer()?;
738 assert_eq!(dm.used_disk_space(), 0);
740 assert_eq!(temp_file.size().unwrap(), 0);
741
742 writer.write_all(b"hello world")?;
744
745 let expected_usage = temp_file.size().unwrap();
747 assert!(expected_usage > 0);
748 assert_eq!(dm.used_disk_space(), expected_usage);
749
750 writer.write_all(b"more_data")?;
752
753 let new_usage = temp_file.size().unwrap();
755 assert!(new_usage > expected_usage);
756 assert_eq!(dm.used_disk_space(), new_usage);
757
758 drop(temp_file);
760
761 assert_eq!(dm.used_disk_space(), 0);
763
764 Ok(())
765 }
766
767 #[test]
768 fn test_disk_usage_with_clones() -> Result<()> {
769 let dm = Arc::new(DiskManagerBuilder::default().build()?);
770 let temp_file = dm.create_tmp_file("Testing")?;
771
772 let mut writer = temp_file.open_writer()?;
774 writer.write_all(b"test data")?;
775
776 let usage_after_write = temp_file.size().unwrap();
777 assert!(usage_after_write > 0);
778 assert_eq!(dm.used_disk_space(), usage_after_write);
779
780 let clone1 = Arc::clone(&temp_file);
782 let clone2 = Arc::clone(&temp_file);
783
784 assert_eq!(clone1.size().unwrap(), usage_after_write);
786 assert_eq!(clone2.size().unwrap(), usage_after_write);
787 assert_eq!(dm.used_disk_space(), usage_after_write);
789
790 let mut clone_writer = clone1.open_writer()?;
792 clone_writer.write_all(b" more data")?;
793
794 let new_usage = clone1.size().unwrap();
795 assert!(new_usage > usage_after_write);
796 assert_eq!(temp_file.size().unwrap(), new_usage);
798 assert_eq!(clone2.size().unwrap(), new_usage);
799 assert_eq!(clone1.size().unwrap(), new_usage);
800
801 assert_eq!(dm.used_disk_space(), new_usage);
803
804 drop(clone_writer);
806 drop(clone1);
807
808 assert_eq!(dm.used_disk_space(), new_usage);
810 assert_eq!(temp_file.size().unwrap(), new_usage);
811 assert_eq!(clone2.size().unwrap(), new_usage);
812
813 drop(clone2);
815
816 assert_eq!(dm.used_disk_space(), new_usage);
818 assert_eq!(temp_file.size().unwrap(), new_usage);
819
820 drop(writer);
822 drop(temp_file);
823 assert_eq!(dm.used_disk_space(), 0);
825
826 Ok(())
827 }
828
829 #[test]
830 fn test_disk_usage_clones_dropped_out_of_order() -> Result<()> {
831 let dm = Arc::new(DiskManagerBuilder::default().build()?);
832 let temp_file = dm.create_tmp_file("Testing")?;
833 let mut writer = temp_file.open_writer()?;
834
835 writer.write_all(b"test")?;
837
838 let usage = temp_file.size().unwrap();
839 assert_eq!(dm.used_disk_space(), usage);
840
841 let clone1 = Arc::clone(&temp_file);
843 let clone2 = Arc::clone(&temp_file);
844 let clone3 = Arc::clone(&temp_file);
845
846 drop(temp_file);
848
849 assert_eq!(dm.used_disk_space(), usage);
851 assert_eq!(clone1.size().unwrap(), usage);
852
853 drop(clone2);
855 assert_eq!(dm.used_disk_space(), usage);
856
857 drop(clone1);
858 assert_eq!(dm.used_disk_space(), usage);
859
860 drop(clone3);
862
863 assert_eq!(dm.used_disk_space(), 0);
865
866 Ok(())
867 }
868
869 #[test]
870 fn test_disk_usage_multiple_files() -> Result<()> {
871 let dm = Arc::new(DiskManagerBuilder::default().build()?);
872
873 let file1 = dm.create_tmp_file("Testing1")?;
875 let file2 = dm.create_tmp_file("Testing2")?;
876
877 let mut writer1 = file1.open_writer()?;
878 let mut writer2 = file2.open_writer()?;
879
880 writer1.write_all(b"file1")?;
882 let usage1 = file1.size().unwrap();
883
884 assert_eq!(dm.used_disk_space(), usage1);
885
886 writer2.write_all(b"file2 data")?;
888 let usage2 = file2.size().unwrap();
889
890 assert_eq!(dm.used_disk_space(), usage1 + usage2);
892
893 drop(file1);
895
896 assert_eq!(dm.used_disk_space(), usage2);
898
899 drop(file2);
901
902 assert_eq!(dm.used_disk_space(), 0);
904
905 Ok(())
906 }
907
908 #[test]
909 fn test_dynamic_limit_adjustment_through_shared_ref() -> Result<()> {
910 let dm = DiskManager::builder()
914 .with_max_temp_directory_size(1024)
915 .build()?;
916 let dm = Arc::new(dm);
917
918 assert_eq!(dm.max_temp_directory_size(), 1024);
919
920 dm.set_max_temp_directory_size(2048)?;
922 assert_eq!(dm.max_temp_directory_size(), 2048);
923
924 dm.set_max_temp_directory_size(512)?;
926 assert_eq!(dm.max_temp_directory_size(), 512);
927
928 Ok(())
929 }
930
931 #[test]
932 fn test_dynamic_limit_concurrent_access() -> Result<()> {
933 let dm = Arc::new(
935 DiskManager::builder()
936 .with_max_temp_directory_size(1000)
937 .build()?,
938 );
939
940 let handles: Vec<_> = (0..8)
941 .map(|i| {
942 let dm = Arc::clone(&dm);
943 std::thread::spawn(move || {
944 let new_limit = (i + 1) * 1000;
946 dm.set_max_temp_directory_size(new_limit).unwrap();
947 let current = dm.max_temp_directory_size();
949 assert!((1000..=8000).contains(¤t));
950 })
951 })
952 .collect();
953
954 for h in handles {
955 h.join().unwrap();
956 }
957
958 let final_val = dm.max_temp_directory_size();
960 assert!((1000..=8000).contains(&final_val));
961
962 Ok(())
963 }
964
965 #[test]
966 fn test_max_spill_merge_fan_in_builder_and_dynamic_update() -> Result<()> {
967 let dm = Arc::new(
968 DiskManager::builder()
969 .with_max_spill_merge_fan_in(8)
970 .build()?,
971 );
972
973 assert_eq!(dm.max_spill_merge_fan_in(), 8);
974
975 dm.set_max_spill_merge_fan_in(4);
976 assert_eq!(dm.max_spill_merge_fan_in(), 4);
977
978 dm.set_max_spill_merge_fan_in(0);
979 assert_eq!(dm.max_spill_merge_fan_in(), 0);
980
981 Ok(())
982 }
983
984 #[test]
985 fn test_disabled_disk_manager_rejects_nonzero_limit() -> Result<()> {
986 let dm = DiskManager::builder()
987 .with_mode(DiskManagerMode::Disabled)
988 .build()?;
989 let dm = Arc::new(dm);
990
991 let result = dm.set_max_temp_directory_size(1024);
993 assert!(result.is_err());
994
995 assert!(dm.set_max_temp_directory_size(0).is_ok());
997
998 Ok(())
999 }
1000
1001 #[test]
1002 fn test_limit_decrease_below_current_usage() -> Result<()> {
1003 let dm = DiskManager::builder()
1017 .with_max_temp_directory_size(100 * 1024 * 1024 * 1024) .build()?;
1019 let dm = Arc::new(dm);
1020
1021 dm.used_disk_space
1023 .store(80 * 1024 * 1024 * 1024, Ordering::Relaxed);
1024
1025 assert_eq!(dm.max_temp_directory_size(), 100 * 1024 * 1024 * 1024);
1026 assert_eq!(dm.used_disk_space(), 80 * 1024 * 1024 * 1024);
1027
1028 dm.set_max_temp_directory_size(60 * 1024 * 1024 * 1024)?;
1030 assert_eq!(dm.max_temp_directory_size(), 60 * 1024 * 1024 * 1024);
1031
1032 assert_eq!(dm.used_disk_space(), 80 * 1024 * 1024 * 1024);
1035
1036 dm.used_disk_space
1042 .store(50 * 1024 * 1024 * 1024, Ordering::Relaxed);
1043
1044 assert!(dm.used_disk_space() < dm.max_temp_directory_size());
1046
1047 Ok(())
1048 }
1049
1050 #[test]
1051 fn test_limit_decrease_with_concurrent_queries() -> Result<()> {
1052 let dm = Arc::new(
1059 DiskManager::builder()
1060 .with_max_temp_directory_size(100 * 1024 * 1024) .build()?,
1062 );
1063
1064 let barrier = Arc::new(std::sync::Barrier::new(5));
1065
1066 let spill_handles: Vec<_> = (0..4)
1068 .map(|_| {
1069 let dm = Arc::clone(&dm);
1070 let barrier = Arc::clone(&barrier);
1071 std::thread::spawn(move || {
1072 barrier.wait();
1073 dm.used_disk_space
1075 .fetch_add(10 * 1024 * 1024, Ordering::Relaxed);
1076 std::thread::sleep(std::time::Duration::from_millis(10));
1077 dm.used_disk_space
1079 .fetch_sub(10 * 1024 * 1024, Ordering::Relaxed);
1080 })
1081 })
1082 .collect();
1083
1084 let dm_resize = Arc::clone(&dm);
1086 let resize_barrier = Arc::clone(&barrier);
1087 let resize_handle = std::thread::spawn(move || {
1088 resize_barrier.wait();
1089 dm_resize
1091 .set_max_temp_directory_size(30 * 1024 * 1024) .unwrap();
1093 });
1094
1095 for h in spill_handles {
1096 h.join().unwrap();
1097 }
1098 resize_handle.join().unwrap();
1099
1100 assert_eq!(dm.max_temp_directory_size(), 30 * 1024 * 1024);
1105 assert_eq!(dm.used_disk_space(), 0);
1106
1107 Ok(())
1108 }
1109
1110 #[test]
1111 fn test_rollback_on_limit_exceeded_then_drop_returns_to_zero() -> Result<()> {
1112 let dm = Arc::new(
1119 DiskManager::builder()
1120 .with_max_temp_directory_size(10 * 1024 * 1024) .build()?,
1122 );
1123
1124 let file = dm.create_tmp_file("test_rollback")?;
1125
1126 let mut writer = file.open_writer()?;
1127
1128 {
1130 let data = vec![0u8; 1024]; writer.write_all(&data)?;
1132 }
1133
1134 let usage_after_first_write = dm.used_disk_space();
1135 assert!(usage_after_first_write > 0);
1136
1137 {
1139 let data = vec![0u8; 4 * 1024]; writer.write_all(&data)?;
1141 }
1142
1143 let usage_after_second_write = dm.used_disk_space();
1144 assert!(usage_after_second_write > usage_after_first_write);
1145
1146 dm.set_max_temp_directory_size(1)?;
1148
1149 {
1151 let data = vec![0u8; 2 * 1024]; let result = writer.write_all(&data);
1155
1156 assert!(result.is_err());
1157 assert!(
1158 result
1159 .unwrap_err()
1160 .to_string()
1161 .contains("exceeded the allowable limit")
1162 );
1163 }
1164
1165 assert_eq!(dm.used_disk_space(), usage_after_second_write);
1168
1169 drop(writer);
1171 drop(file);
1172
1173 assert_eq!(dm.used_disk_space(), 0);
1175
1176 Ok(())
1177 }
1178}