use crate::spill_file::{SpillFile, SpillWriter, TempFileFactory};
use bytes::Bytes;
use datafusion_common::human_readable_size;
use datafusion_common::{DataFusionError, Result, config_err, resources_datafusion_err};
#[cfg(not(target_arch = "wasm32"))]
use futures::StreamExt;
use log::debug;
use parking_lot::Mutex;
use rand::{Rng, rng};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use tempfile::{Builder, NamedTempFile, TempDir};
pub const DEFAULT_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; pub const DEFAULT_MAX_SPILL_MERGE_FAN_IN: usize = 0;
#[derive(Clone)]
pub struct DiskManagerBuilder {
mode: DiskManagerMode,
max_temp_directory_size: u64,
max_spill_merge_fan_in: usize,
}
impl Debug for DiskManagerBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiskManagerBuilder")
.field("mode", &self.mode)
.field("max_temp_directory_size", &self.max_temp_directory_size)
.finish()
}
}
impl Default for DiskManagerBuilder {
fn default() -> Self {
Self {
mode: DiskManagerMode::OsTmpDirectory,
max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE,
max_spill_merge_fan_in: DEFAULT_MAX_SPILL_MERGE_FAN_IN,
}
}
}
impl DiskManagerBuilder {
pub fn set_mode(&mut self, mode: DiskManagerMode) {
self.mode = mode;
}
pub fn with_mode(mut self, mode: DiskManagerMode) -> Self {
self.set_mode(mode);
self
}
pub fn set_temp_file_factory(&mut self, temp_file_factory: Arc<dyn TempFileFactory>) {
self.mode = DiskManagerMode::Custom(temp_file_factory);
}
pub fn with_temp_file_factory(
mut self,
temp_file_factory: Arc<dyn TempFileFactory>,
) -> Self {
self.set_temp_file_factory(temp_file_factory);
self
}
pub fn set_max_temp_directory_size(&mut self, value: u64) {
self.max_temp_directory_size = value;
}
pub fn with_max_temp_directory_size(mut self, value: u64) -> Self {
self.set_max_temp_directory_size(value);
self
}
pub fn set_max_spill_merge_fan_in(&mut self, value: usize) {
self.max_spill_merge_fan_in = value;
}
pub fn with_max_spill_merge_fan_in(mut self, value: usize) -> Self {
self.set_max_spill_merge_fan_in(value);
self
}
pub fn build(self) -> Result<DiskManager> {
match self.mode {
DiskManagerMode::OsTmpDirectory => Ok(DiskManager {
local_dirs: Mutex::new(Some(vec![])),
max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
used_disk_space: Arc::new(AtomicU64::new(0)),
active_files_count: Arc::new(AtomicUsize::new(0)),
factory: None,
}),
DiskManagerMode::Directories(conf_dirs) => {
let local_dirs = create_local_dirs(&conf_dirs)?;
debug!(
"Created local dirs {local_dirs:?} as DataFusion working directory"
);
Ok(DiskManager {
local_dirs: Mutex::new(Some(local_dirs)),
max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
used_disk_space: Arc::new(AtomicU64::new(0)),
active_files_count: Arc::new(AtomicUsize::new(0)),
factory: None,
})
}
DiskManagerMode::Disabled => Ok(DiskManager {
local_dirs: Mutex::new(None),
max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
used_disk_space: Arc::new(AtomicU64::new(0)),
active_files_count: Arc::new(AtomicUsize::new(0)),
factory: None,
}),
DiskManagerMode::Custom(factory) => Ok(DiskManager {
local_dirs: Mutex::new(None),
max_temp_directory_size: AtomicU64::new(self.max_temp_directory_size),
max_spill_merge_fan_in: AtomicUsize::new(self.max_spill_merge_fan_in),
used_disk_space: Arc::new(AtomicU64::new(0)),
active_files_count: Arc::new(AtomicUsize::new(0)),
factory: Some(factory),
}),
}
}
}
#[derive(Clone, Default)]
pub enum DiskManagerMode {
#[default]
OsTmpDirectory,
Directories(Vec<PathBuf>),
Custom(Arc<dyn TempFileFactory>),
Disabled,
}
impl Debug for DiskManagerMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OsTmpDirectory => write!(f, "OsTmpDirectory"),
Self::Directories(dirs) => f.debug_tuple("Directories").field(dirs).finish(),
Self::Disabled => write!(f, "Disabled"),
Self::Custom(_) => write!(f, "Custom(Arc<dyn TempFileFactory>)"),
}
}
}
pub struct DiskManager {
local_dirs: Mutex<Option<Vec<Arc<TempDir>>>>,
max_temp_directory_size: AtomicU64,
max_spill_merge_fan_in: AtomicUsize,
used_disk_space: Arc<AtomicU64>,
active_files_count: Arc<AtomicUsize>,
factory: Option<Arc<dyn TempFileFactory>>,
}
impl Debug for DiskManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiskManager")
.field("local_dirs", &self.local_dirs)
.field("max_temp_directory_size", &self.max_temp_directory_size)
.field("used_disk_space", &self.used_disk_space)
.field("active_files_count", &self.active_files_count)
.field("factory", &self.factory.is_some())
.finish()
}
}
#[derive(Debug, Clone, Copy)]
pub struct SpillingProgress {
pub current_bytes: u64,
pub active_files_count: usize,
}
impl DiskManager {
pub fn builder() -> DiskManagerBuilder {
DiskManagerBuilder::default()
}
pub fn set_max_temp_directory_size(
&self,
max_temp_directory_size: u64,
) -> Result<()> {
if self.local_dirs.lock().is_none()
&& max_temp_directory_size != 0
&& self.factory.is_none()
{
return config_err!(
"Cannot set max temp directory size for a disk manager that spilling is disabled"
);
}
self.max_temp_directory_size
.store(max_temp_directory_size, Ordering::Relaxed);
Ok(())
}
#[deprecated(
since = "54.0.0",
note = "Use `set_max_temp_directory_size` directly, it now takes &self"
)]
pub fn set_arc_max_temp_directory_size(
this: &Arc<Self>,
max_temp_directory_size: u64,
) -> Result<()> {
this.set_max_temp_directory_size(max_temp_directory_size)
}
pub fn with_max_temp_directory_size(
self,
max_temp_directory_size: u64,
) -> Result<Self> {
self.set_max_temp_directory_size(max_temp_directory_size)?;
Ok(self)
}
pub fn used_disk_space(&self) -> u64 {
self.used_disk_space.load(Ordering::Relaxed)
}
pub fn max_temp_directory_size(&self) -> u64 {
self.max_temp_directory_size.load(Ordering::Relaxed)
}
pub fn set_max_spill_merge_fan_in(&self, max_spill_merge_fan_in: usize) {
self.max_spill_merge_fan_in
.store(max_spill_merge_fan_in, Ordering::Relaxed);
}
pub fn max_spill_merge_fan_in(&self) -> usize {
self.max_spill_merge_fan_in.load(Ordering::Relaxed)
}
pub fn spilling_progress(&self) -> SpillingProgress {
SpillingProgress {
current_bytes: self.used_disk_space.load(Ordering::Relaxed),
active_files_count: self.active_files_count.load(Ordering::Relaxed),
}
}
pub fn temp_dir_paths(&self) -> Vec<PathBuf> {
self.local_dirs
.lock()
.as_ref()
.map(|dirs| {
dirs.iter()
.map(|temp_dir| temp_dir.path().to_path_buf())
.collect()
})
.unwrap_or_default()
}
pub fn tmp_files_enabled(&self) -> bool {
self.factory.is_some() || self.local_dirs.lock().is_some()
}
pub fn create_tmp_file(
self: &Arc<Self>,
request_description: &str,
) -> Result<Arc<dyn SpillFile>> {
if let Some(factory) = &self.factory {
return factory.create_temp_file(request_description);
}
let mut guard = self.local_dirs.lock();
let local_dirs = guard.as_mut().ok_or_else(|| {
resources_datafusion_err!(
"Memory Exhausted while {request_description} (DiskManager is disabled)"
)
})?;
if local_dirs.is_empty() {
let tempdir = tempfile::tempdir().map_err(DataFusionError::IoError)?;
debug!(
"Created directory '{:?}' as DataFusion tempfile directory for {}",
tempdir.path().to_string_lossy(),
request_description,
);
local_dirs.push(Arc::new(tempdir));
}
let dir_index = rng().random_range(0..local_dirs.len());
self.active_files_count.fetch_add(1, Ordering::Relaxed);
Ok(Arc::new(RefCountedTempFile {
parent_temp_dir: Arc::clone(&local_dirs[dir_index]),
tempfile: Arc::new(
Builder::new()
.tempfile_in(local_dirs[dir_index].as_ref())
.map_err(DataFusionError::IoError)?,
),
current_file_disk_usage: Arc::new(AtomicU64::new(0)),
disk_manager: Arc::clone(self),
}))
}
}
#[derive(Debug)]
pub struct RefCountedTempFile {
parent_temp_dir: Arc<TempDir>,
tempfile: Arc<NamedTempFile>,
current_file_disk_usage: Arc<AtomicU64>,
disk_manager: Arc<DiskManager>,
}
impl Clone for RefCountedTempFile {
fn clone(&self) -> Self {
Self {
parent_temp_dir: Arc::clone(&self.parent_temp_dir),
tempfile: Arc::clone(&self.tempfile),
current_file_disk_usage: Arc::clone(&self.current_file_disk_usage),
disk_manager: Arc::clone(&self.disk_manager),
}
}
}
impl RefCountedTempFile {
pub fn path(&self) -> &Path {
self.tempfile.path()
}
pub fn inner(&self) -> &NamedTempFile {
self.tempfile.as_ref()
}
fn current_disk_usage(&self) -> u64 {
self.current_file_disk_usage.load(Ordering::Relaxed)
}
}
impl Drop for RefCountedTempFile {
fn drop(&mut self) {
if Arc::strong_count(&self.tempfile) == 1 {
let current_usage = self.current_file_disk_usage.load(Ordering::Relaxed);
self.disk_manager
.used_disk_space
.fetch_sub(current_usage, Ordering::Relaxed);
self.disk_manager
.active_files_count
.fetch_sub(1, Ordering::Relaxed);
}
}
}
fn create_local_dirs(local_dirs: &[PathBuf]) -> Result<Vec<Arc<TempDir>>> {
local_dirs
.iter()
.map(|root| {
if !Path::new(root).exists() {
std::fs::create_dir(root)?;
}
Builder::new()
.prefix("datafusion-")
.tempdir_in(root)
.map_err(DataFusionError::IoError)
})
.map(|result| result.map(Arc::new))
.collect()
}
pub struct FileSpillWriter {
file: std::fs::File,
disk_manager: Arc<DiskManager>,
current_file_disk_usage: Arc<AtomicU64>,
}
impl std::io::Write for FileSpillWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let len = buf.len() as u64;
if len == 0 {
return Ok(0);
}
let new_global = self
.disk_manager
.used_disk_space
.fetch_add(len, Ordering::Relaxed)
+ len;
let limit = self.disk_manager.max_temp_directory_size();
if new_global > limit {
self.disk_manager
.used_disk_space
.fetch_sub(len, Ordering::Relaxed);
return Err(std::io::Error::other(format!(
"The used disk space during the spilling process has exceeded the allowable limit of {}. \
Please try increasing the config: `datafusion.runtime.max_temp_directory_size`.",
human_readable_size(limit as usize)
)));
}
self.file.write_all(buf).map_err(DataFusionError::IoError)?;
self.current_file_disk_usage
.fetch_add(len, Ordering::Relaxed);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}
impl SpillWriter for FileSpillWriter {
fn finish(&mut self) -> Result<()> {
Ok(())
}
}
impl SpillFile for RefCountedTempFile {
fn path(&self) -> Option<&Path> {
Some(self.tempfile.path())
}
fn size(&self) -> Option<u64> {
Some(self.current_disk_usage())
}
#[cfg(not(target_arch = "wasm32"))]
fn read_stream(
&self,
) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<Bytes>> + Send>>>
{
let path = self.path().to_owned();
let stream =
futures::stream::once(async move {
tokio::fs::File::open(&path)
.await
.map_err(DataFusionError::IoError)
})
.flat_map(
|open_result| -> std::pin::Pin<
Box<dyn futures::Stream<Item = Result<Bytes>> + Send>,
> {
match open_result {
Ok(file) => Box::pin(
tokio_util::io::ReaderStream::with_capacity(file, 128 * 1024)
.map(|r| r.map_err(DataFusionError::IoError)),
),
Err(e) => Box::pin(futures::stream::once(async move { Err(e) })),
}
},
);
Ok(Box::pin(stream))
}
#[cfg(target_arch = "wasm32")]
fn read_stream(
&self,
) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<Bytes>> + Send>>>
{
datafusion_common::exec_err!(
"Default OS file spilling is not supported on WASM. Configure DiskManager with a Custom TempFileFactory."
)
}
fn open_writer(&self) -> Result<Box<dyn SpillWriter>> {
let file = self
.tempfile
.as_file()
.try_clone()
.map_err(DataFusionError::IoError)?;
Ok(Box::new(FileSpillWriter {
file,
disk_manager: Arc::clone(&self.disk_manager),
current_file_disk_usage: Arc::clone(&self.current_file_disk_usage),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lazy_temp_dir_creation() -> Result<()> {
let dm = Arc::new(DiskManagerBuilder::default().build()?);
assert_eq!(0, local_dir_snapshot(&dm).len());
let actual = dm.create_tmp_file("Testing")?;
assert_eq!(1, local_dir_snapshot(&dm).len());
let local_dirs = local_dir_snapshot(&dm);
assert_path_in_dirs(
actual.path().unwrap(),
local_dirs.iter().map(|p| p.as_path()),
);
Ok(())
}
fn local_dir_snapshot(dm: &DiskManager) -> Vec<PathBuf> {
dm.local_dirs
.lock()
.iter()
.flatten()
.map(|p| p.path().into())
.collect()
}
#[test]
fn file_in_right_dir() -> Result<()> {
let local_dir1 = TempDir::new()?;
let local_dir2 = TempDir::new()?;
let local_dir3 = TempDir::new()?;
let local_dirs = vec![local_dir1.path(), local_dir2.path(), local_dir3.path()];
let dm = Arc::new(
DiskManagerBuilder::default()
.with_mode(DiskManagerMode::Directories(
local_dirs.iter().map(|p| p.into()).collect(),
))
.build()?,
);
assert!(dm.tmp_files_enabled());
let actual = dm.create_tmp_file("Testing")?;
assert_path_in_dirs(actual.path().unwrap(), local_dirs.into_iter());
Ok(())
}
#[test]
fn test_disabled_disk_manager() {
let manager = Arc::new(
DiskManagerBuilder::default()
.with_mode(DiskManagerMode::Disabled)
.build()
.unwrap(),
);
assert!(!manager.tmp_files_enabled());
match manager.create_tmp_file("Testing") {
Err(e) => {
assert_eq!(
e.strip_backtrace(),
"Resources exhausted: Memory Exhausted while Testing (DiskManager is disabled)"
);
}
Ok(_) => {
panic!("Expected DiskManager to fail creating a file when disabled!")
}
}
}
#[test]
fn test_disk_manager_create_spill_folder() {
let dir = TempDir::new().unwrap();
DiskManagerBuilder::default()
.with_mode(DiskManagerMode::Directories(vec![dir.path().to_path_buf()]))
.build()
.unwrap();
}
fn assert_path_in_dirs<'a>(
file_path: &'a Path,
dirs: impl Iterator<Item = &'a Path>,
) {
let dirs: Vec<&Path> = dirs.collect();
let found = dirs.iter().any(|dir_path| {
file_path
.ancestors()
.any(|candidate_path| *dir_path == candidate_path)
});
assert!(found, "Can't find {file_path:?} in dirs: {dirs:?}");
}
#[test]
fn test_temp_file_still_alive_after_disk_manager_dropped() -> Result<()> {
let dm = Arc::new(DiskManagerBuilder::default().build()?);
let temp_file = dm.create_tmp_file("Testing")?;
let temp_file_path = temp_file.path().unwrap().to_owned();
assert!(temp_file_path.exists());
drop(dm);
assert!(temp_file_path.exists());
drop(temp_file);
assert!(!temp_file_path.exists());
let local_dir1 = TempDir::new()?;
let local_dir2 = TempDir::new()?;
let local_dir3 = TempDir::new()?;
let local_dirs = [local_dir1.path(), local_dir2.path(), local_dir3.path()];
let dm = Arc::new(
DiskManagerBuilder::default()
.with_mode(DiskManagerMode::Directories(
local_dirs.iter().map(|p| p.into()).collect(),
))
.build()?,
);
let temp_file = dm.create_tmp_file("Testing")?;
let temp_file_path = temp_file.path().unwrap().to_owned();
assert!(temp_file_path.exists());
drop(dm);
assert!(temp_file_path.exists());
drop(temp_file);
assert!(!temp_file_path.exists());
Ok(())
}
#[test]
fn test_disk_usage_basic() -> Result<()> {
let dm = Arc::new(DiskManagerBuilder::default().build()?);
let temp_file = dm.create_tmp_file("Testing")?;
let mut writer = temp_file.open_writer()?;
assert_eq!(dm.used_disk_space(), 0);
assert_eq!(temp_file.size().unwrap(), 0);
writer.write_all(b"hello world")?;
let expected_usage = temp_file.size().unwrap();
assert!(expected_usage > 0);
assert_eq!(dm.used_disk_space(), expected_usage);
writer.write_all(b"more_data")?;
let new_usage = temp_file.size().unwrap();
assert!(new_usage > expected_usage);
assert_eq!(dm.used_disk_space(), new_usage);
drop(temp_file);
assert_eq!(dm.used_disk_space(), 0);
Ok(())
}
#[test]
fn test_disk_usage_with_clones() -> Result<()> {
let dm = Arc::new(DiskManagerBuilder::default().build()?);
let temp_file = dm.create_tmp_file("Testing")?;
let mut writer = temp_file.open_writer()?;
writer.write_all(b"test data")?;
let usage_after_write = temp_file.size().unwrap();
assert!(usage_after_write > 0);
assert_eq!(dm.used_disk_space(), usage_after_write);
let clone1 = Arc::clone(&temp_file);
let clone2 = Arc::clone(&temp_file);
assert_eq!(clone1.size().unwrap(), usage_after_write);
assert_eq!(clone2.size().unwrap(), usage_after_write);
assert_eq!(dm.used_disk_space(), usage_after_write);
let mut clone_writer = clone1.open_writer()?;
clone_writer.write_all(b" more data")?;
let new_usage = clone1.size().unwrap();
assert!(new_usage > usage_after_write);
assert_eq!(temp_file.size().unwrap(), new_usage);
assert_eq!(clone2.size().unwrap(), new_usage);
assert_eq!(clone1.size().unwrap(), new_usage);
assert_eq!(dm.used_disk_space(), new_usage);
drop(clone_writer);
drop(clone1);
assert_eq!(dm.used_disk_space(), new_usage);
assert_eq!(temp_file.size().unwrap(), new_usage);
assert_eq!(clone2.size().unwrap(), new_usage);
drop(clone2);
assert_eq!(dm.used_disk_space(), new_usage);
assert_eq!(temp_file.size().unwrap(), new_usage);
drop(writer);
drop(temp_file);
assert_eq!(dm.used_disk_space(), 0);
Ok(())
}
#[test]
fn test_disk_usage_clones_dropped_out_of_order() -> Result<()> {
let dm = Arc::new(DiskManagerBuilder::default().build()?);
let temp_file = dm.create_tmp_file("Testing")?;
let mut writer = temp_file.open_writer()?;
writer.write_all(b"test")?;
let usage = temp_file.size().unwrap();
assert_eq!(dm.used_disk_space(), usage);
let clone1 = Arc::clone(&temp_file);
let clone2 = Arc::clone(&temp_file);
let clone3 = Arc::clone(&temp_file);
drop(temp_file);
assert_eq!(dm.used_disk_space(), usage);
assert_eq!(clone1.size().unwrap(), usage);
drop(clone2);
assert_eq!(dm.used_disk_space(), usage);
drop(clone1);
assert_eq!(dm.used_disk_space(), usage);
drop(clone3);
assert_eq!(dm.used_disk_space(), 0);
Ok(())
}
#[test]
fn test_disk_usage_multiple_files() -> Result<()> {
let dm = Arc::new(DiskManagerBuilder::default().build()?);
let file1 = dm.create_tmp_file("Testing1")?;
let file2 = dm.create_tmp_file("Testing2")?;
let mut writer1 = file1.open_writer()?;
let mut writer2 = file2.open_writer()?;
writer1.write_all(b"file1")?;
let usage1 = file1.size().unwrap();
assert_eq!(dm.used_disk_space(), usage1);
writer2.write_all(b"file2 data")?;
let usage2 = file2.size().unwrap();
assert_eq!(dm.used_disk_space(), usage1 + usage2);
drop(file1);
assert_eq!(dm.used_disk_space(), usage2);
drop(file2);
assert_eq!(dm.used_disk_space(), 0);
Ok(())
}
#[test]
fn test_dynamic_limit_adjustment_through_shared_ref() -> Result<()> {
let dm = DiskManager::builder()
.with_max_temp_directory_size(1024)
.build()?;
let dm = Arc::new(dm);
assert_eq!(dm.max_temp_directory_size(), 1024);
dm.set_max_temp_directory_size(2048)?;
assert_eq!(dm.max_temp_directory_size(), 2048);
dm.set_max_temp_directory_size(512)?;
assert_eq!(dm.max_temp_directory_size(), 512);
Ok(())
}
#[test]
fn test_dynamic_limit_concurrent_access() -> Result<()> {
let dm = Arc::new(
DiskManager::builder()
.with_max_temp_directory_size(1000)
.build()?,
);
let handles: Vec<_> = (0..8)
.map(|i| {
let dm = Arc::clone(&dm);
std::thread::spawn(move || {
let new_limit = (i + 1) * 1000;
dm.set_max_temp_directory_size(new_limit).unwrap();
let current = dm.max_temp_directory_size();
assert!((1000..=8000).contains(¤t));
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let final_val = dm.max_temp_directory_size();
assert!((1000..=8000).contains(&final_val));
Ok(())
}
#[test]
fn test_max_spill_merge_fan_in_builder_and_dynamic_update() -> Result<()> {
let dm = Arc::new(
DiskManager::builder()
.with_max_spill_merge_fan_in(8)
.build()?,
);
assert_eq!(dm.max_spill_merge_fan_in(), 8);
dm.set_max_spill_merge_fan_in(4);
assert_eq!(dm.max_spill_merge_fan_in(), 4);
dm.set_max_spill_merge_fan_in(0);
assert_eq!(dm.max_spill_merge_fan_in(), 0);
Ok(())
}
#[test]
fn test_disabled_disk_manager_rejects_nonzero_limit() -> Result<()> {
let dm = DiskManager::builder()
.with_mode(DiskManagerMode::Disabled)
.build()?;
let dm = Arc::new(dm);
let result = dm.set_max_temp_directory_size(1024);
assert!(result.is_err());
assert!(dm.set_max_temp_directory_size(0).is_ok());
Ok(())
}
#[test]
fn test_limit_decrease_below_current_usage() -> Result<()> {
let dm = DiskManager::builder()
.with_max_temp_directory_size(100 * 1024 * 1024 * 1024) .build()?;
let dm = Arc::new(dm);
dm.used_disk_space
.store(80 * 1024 * 1024 * 1024, Ordering::Relaxed);
assert_eq!(dm.max_temp_directory_size(), 100 * 1024 * 1024 * 1024);
assert_eq!(dm.used_disk_space(), 80 * 1024 * 1024 * 1024);
dm.set_max_temp_directory_size(60 * 1024 * 1024 * 1024)?;
assert_eq!(dm.max_temp_directory_size(), 60 * 1024 * 1024 * 1024);
assert_eq!(dm.used_disk_space(), 80 * 1024 * 1024 * 1024);
dm.used_disk_space
.store(50 * 1024 * 1024 * 1024, Ordering::Relaxed);
assert!(dm.used_disk_space() < dm.max_temp_directory_size());
Ok(())
}
#[test]
fn test_limit_decrease_with_concurrent_queries() -> Result<()> {
let dm = Arc::new(
DiskManager::builder()
.with_max_temp_directory_size(100 * 1024 * 1024) .build()?,
);
let barrier = Arc::new(std::sync::Barrier::new(5));
let spill_handles: Vec<_> = (0..4)
.map(|_| {
let dm = Arc::clone(&dm);
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait();
dm.used_disk_space
.fetch_add(10 * 1024 * 1024, Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_millis(10));
dm.used_disk_space
.fetch_sub(10 * 1024 * 1024, Ordering::Relaxed);
})
})
.collect();
let dm_resize = Arc::clone(&dm);
let resize_barrier = Arc::clone(&barrier);
let resize_handle = std::thread::spawn(move || {
resize_barrier.wait();
dm_resize
.set_max_temp_directory_size(30 * 1024 * 1024) .unwrap();
});
for h in spill_handles {
h.join().unwrap();
}
resize_handle.join().unwrap();
assert_eq!(dm.max_temp_directory_size(), 30 * 1024 * 1024);
assert_eq!(dm.used_disk_space(), 0);
Ok(())
}
#[test]
fn test_rollback_on_limit_exceeded_then_drop_returns_to_zero() -> Result<()> {
let dm = Arc::new(
DiskManager::builder()
.with_max_temp_directory_size(10 * 1024 * 1024) .build()?,
);
let file = dm.create_tmp_file("test_rollback")?;
let mut writer = file.open_writer()?;
{
let data = vec![0u8; 1024]; writer.write_all(&data)?;
}
let usage_after_first_write = dm.used_disk_space();
assert!(usage_after_first_write > 0);
{
let data = vec![0u8; 4 * 1024]; writer.write_all(&data)?;
}
let usage_after_second_write = dm.used_disk_space();
assert!(usage_after_second_write > usage_after_first_write);
dm.set_max_temp_directory_size(1)?;
{
let data = vec![0u8; 2 * 1024];
let result = writer.write_all(&data);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exceeded the allowable limit")
);
}
assert_eq!(dm.used_disk_space(), usage_after_second_write);
drop(writer);
drop(file);
assert_eq!(dm.used_disk_space(), 0);
Ok(())
}
}