Skip to main content

datafusion_execution/
disk_manager.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! [`DiskManager`]: Manages files generated during query execution
19
20use 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; // 100GB
35pub const DEFAULT_MAX_SPILL_MERGE_FAN_IN: usize = 0;
36
37/// Builder pattern for the [DiskManager] structure
38#[derive(Clone)]
39pub struct DiskManagerBuilder {
40    /// The storage mode of the disk manager
41    mode: DiskManagerMode,
42    /// The maximum amount of data (in bytes) stored inside the temporary directories.
43    /// Default to 100GB
44    max_temp_directory_size: u64,
45    /// Maximum number of spill files opened by one external merge pass.
46    /// A value of 0 means unlimited.
47    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    /// Configure a custom factory for creating temporary spill files.
78    ///
79    /// This sets the disk manager mode to [`DiskManagerMode::Custom`], so
80    /// operators that spill during query execution create files through the
81    /// provided [`TempFileFactory`] instead of using local temporary files.
82    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    /// Configure a custom factory for creating temporary spill files.
87    ///
88    /// See details on [`Self::set_temp_file_factory`].
89    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    /// Create a DiskManager given the builder
116    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    /// Create a new [DiskManager] that creates temporary files within
163    /// a temporary directory chosen by the OS
164    #[default]
165    OsTmpDirectory,
166
167    /// Create a new [DiskManager] that creates temporary files within
168    /// the specified directories. One of the directories will be chosen
169    /// at random for each temporary file created.
170    Directories(Vec<PathBuf>),
171
172    /// Create a new [DiskManager] with a cutstom backend
173    Custom(Arc<dyn TempFileFactory>),
174
175    /// Disable disk manager, attempts to create temporary files will error
176    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
190/// Manages files generated during query execution, e.g. spill files generated
191/// while processing dataset larger than available memory.
192pub struct DiskManager {
193    /// TempDirs to put temporary files in.
194    ///
195    /// If `Some(vec![])` a new OS specified temporary directory will be created
196    /// If `None` an error will be returned (configured not to spill)
197    local_dirs: Mutex<Option<Vec<Arc<TempDir>>>>,
198    /// The maximum amount of data (in bytes) stored inside the temporary directories.
199    /// Default to 100GB. Stored as `AtomicU64` so it can be adjusted at runtime
200    /// without requiring exclusive (`&mut`) access to the `DiskManager`.
201    max_temp_directory_size: AtomicU64,
202    /// Maximum number of spill files opened by one external merge pass.
203    /// A value of 0 preserves the memory-driven, unbounded behavior.
204    max_spill_merge_fan_in: AtomicUsize,
205    /// Used disk space in the temporary directories. Now only spilled data for
206    /// external executors are counted.
207    used_disk_space: Arc<AtomicU64>,
208    /// Number of active temporary files created by this disk manager
209    active_files_count: Arc<AtomicUsize>,
210    /// Factory
211    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/// Information about the current disk usage for spilling
225#[derive(Debug, Clone, Copy)]
226pub struct SpillingProgress {
227    /// Total bytes currently used on disk for spilling
228    pub current_bytes: u64,
229    /// Total number of active spill files
230    pub active_files_count: usize,
231}
232
233impl DiskManager {
234    /// Creates a builder for [DiskManager]
235    pub fn builder() -> DiskManagerBuilder {
236        DiskManagerBuilder::default()
237    }
238
239    /// Atomically set the max temp directory size at runtime.
240    ///
241    /// Takes `&self`, so it works through `Arc<DiskManager>` without requiring
242    /// exclusive access. Takes effect immediately for subsequent spill writes.
243    ///
244    /// Use this when you need to adjust the limit dynamically while queries
245    /// are running (e.g., adapting to available disk space).
246    pub fn set_max_temp_directory_size(
247        &self,
248        max_temp_directory_size: u64,
249    ) -> Result<()> {
250        // If the disk manager is disabled and `max_temp_directory_size` is not 0,
251        // this operation is not meaningful, fail early.
252        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    /// Returns the maximum temporary directory size in bytes
290    pub fn max_temp_directory_size(&self) -> u64 {
291        self.max_temp_directory_size.load(Ordering::Relaxed)
292    }
293
294    /// Atomically set the maximum spill merge fan-in.
295    ///
296    /// A value of 0 disables the cap. Values of 1 are accepted but external
297    /// merge code will still merge at least two spill streams to make progress.
298    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    /// Returns the maximum number of spill files opened by one merge pass.
304    ///
305    /// A value of 0 means unlimited.
306    pub fn max_spill_merge_fan_in(&self) -> usize {
307        self.max_spill_merge_fan_in.load(Ordering::Relaxed)
308    }
309
310    /// Returns the current spilling progress
311    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    /// Returns the temporary directory paths
319    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    /// Return true if this disk manager supports creating temporary
332    /// files. If this returns false, any call to `create_tmp_file`
333    /// will error.
334    pub fn tmp_files_enabled(&self) -> bool {
335        self.factory.is_some() || self.local_dirs.lock().is_some()
336    }
337
338    /// Return a temporary file from a randomized choice in the configured locations
339    ///
340    /// If the file can not be created for some reason, returns an
341    /// error message referencing the request description
342    pub fn create_tmp_file(
343        self: &Arc<Self>,
344        request_description: &str,
345    ) -> Result<Arc<dyn SpillFile>> {
346        // Delegate to custom backend if configured
347        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        // Create a temporary directory if needed
358        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/// A wrapper around a [`NamedTempFile`] that also contains
386/// a reference to its parent temporary directory.
387///
388/// This type is Clone-able, allowing multiple references to the same underlying file.
389/// The file is deleted only when the last reference is dropped.
390///
391/// The parent temporary directory is also kept alive as long as any reference to
392/// this file exists, preventing premature cleanup of the directory.
393///
394/// Once all references to this file are dropped, the file is deleted, and the
395/// disk usage is subtracted from the disk manager's total.
396#[derive(Debug)]
397pub struct RefCountedTempFile {
398    /// The reference to the directory in which temporary files are created to ensure
399    /// it is not cleaned up prior to the NamedTempFile
400    parent_temp_dir: Arc<TempDir>,
401    /// The underlying temporary file, wrapped in Arc to allow cloning
402    tempfile: Arc<NamedTempFile>,
403    /// Tracks the current disk usage of this temporary file.
404    ///
405    /// This is wrapped in `Arc<AtomicU64>` so that all clones share the same
406    /// disk usage tracking, preventing incorrect accounting when clones are dropped.
407    current_file_disk_usage: Arc<AtomicU64>,
408    /// The disk manager that created and manages this temporary file
409    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
437/// When the temporary file is dropped, subtract its disk usage from the disk manager's total
438impl Drop for RefCountedTempFile {
439    fn drop(&mut self) {
440        // Only subtract disk usage when this is the last reference to the file
441        // Check if we're the last one by seeing if there's only one strong reference
442        // left to the underlying tempfile (the one we're holding)
443        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
455/// Setup local dirs by creating one new dir in each of the given dirs
456fn 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        // flush() is handled by Arrow, nothing left to do here
521        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                            // Use a 128KB read buffer. The default 8KB causes excessive async
553                            // poll overhead when reading multi-MB spill files back into memory.
554                            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        // A default configuration should not create temp files until requested
595        let dm = Arc::new(DiskManagerBuilder::default().build()?);
596
597        assert_eq!(0, local_dir_snapshot(&dm).len());
598
599        // can still create a tempfile however:
600        let actual = dm.create_tmp_file("Testing")?;
601
602        // Now the tempdir has been created on demand
603        assert_eq!(1, local_dir_snapshot(&dm).len());
604
605        // the returned tempfile file should be in the temp directory
606        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        // the file should be in one of the specified local directories
642        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    /// Asserts that `file_path` is found anywhere in any of `dir` directories
679    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        // Test for the case using OS arranged temporary directory
697        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        // Test for the case using specified directories
709        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        // Initially, disk usage should be 0
739        assert_eq!(dm.used_disk_space(), 0);
740        assert_eq!(temp_file.size().unwrap(), 0);
741
742        // Write some data to the file
743        writer.write_all(b"hello world")?;
744
745        // Disk usage should now reflect the written data
746        let expected_usage = temp_file.size().unwrap();
747        assert!(expected_usage > 0);
748        assert_eq!(dm.used_disk_space(), expected_usage);
749
750        // Write more data
751        writer.write_all(b"more_data")?;
752
753        // Disk usage should increase
754        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 the file
759        drop(temp_file);
760
761        // Disk usage should return to 0
762        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        // Write some data
773        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        // Clone the file
781        let clone1 = Arc::clone(&temp_file);
782        let clone2 = Arc::clone(&temp_file);
783
784        // All clones should see the same disk usage
785        assert_eq!(clone1.size().unwrap(), usage_after_write);
786        assert_eq!(clone2.size().unwrap(), usage_after_write);
787        // Global disk usage should still be the same (not multiplied by number of clones)
788        assert_eq!(dm.used_disk_space(), usage_after_write);
789
790        // Write more data through one clone
791        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        // All clones should see the updated disk usage
797        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        // Global disk usage should reflect the new size (not multiplied)
802        assert_eq!(dm.used_disk_space(), new_usage);
803
804        // Drop one clone
805        drop(clone_writer);
806        drop(clone1);
807
808        // Disk usage should NOT change (other clones still exist)
809        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 another clone
814        drop(clone2);
815
816        // Disk usage should still NOT change (original still exists)
817        assert_eq!(dm.used_disk_space(), new_usage);
818        assert_eq!(temp_file.size().unwrap(), new_usage);
819
820        // Drop the original
821        drop(writer);
822        drop(temp_file);
823        // Now disk usage should return to 0 (last reference dropped)
824        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        // Write data
836        writer.write_all(b"test")?;
837
838        let usage = temp_file.size().unwrap();
839        assert_eq!(dm.used_disk_space(), usage);
840
841        // Create multiple clones
842        let clone1 = Arc::clone(&temp_file);
843        let clone2 = Arc::clone(&temp_file);
844        let clone3 = Arc::clone(&temp_file);
845
846        // Drop the original first (out of order)
847        drop(temp_file);
848
849        // Disk usage should still be tracked (clones exist)
850        assert_eq!(dm.used_disk_space(), usage);
851        assert_eq!(clone1.size().unwrap(), usage);
852
853        // Drop clones in different order
854        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 the last clone
861        drop(clone3);
862
863        // Now disk usage should be 0
864        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        // Create multiple temp files
874        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        // Write to first file
881        writer1.write_all(b"file1")?;
882        let usage1 = file1.size().unwrap();
883
884        assert_eq!(dm.used_disk_space(), usage1);
885
886        // Write to second file
887        writer2.write_all(b"file2 data")?;
888        let usage2 = file2.size().unwrap();
889
890        // Global usage should be sum of both files
891        assert_eq!(dm.used_disk_space(), usage1 + usage2);
892
893        // Drop first file
894        drop(file1);
895
896        // Usage should only reflect second file
897        assert_eq!(dm.used_disk_space(), usage2);
898
899        // Drop second file
900        drop(file2);
901
902        // Usage should be 0
903        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        // Verify that set_max_temp_directory_size works through &self (not &mut self).
911        // This is the key behavioral change: the limit can be adjusted at runtime
912        // without exclusive access, enabling dynamic resize while queries are running.
913        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        // Adjust through shared reference (simulates concurrent access via Arc)
921        dm.set_max_temp_directory_size(2048)?;
922        assert_eq!(dm.max_temp_directory_size(), 2048);
923
924        // Can also decrease
925        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        // Verify that multiple threads can read and write the limit concurrently
934        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                    // Each thread sets a different limit and reads it back
945                    let new_limit = (i + 1) * 1000;
946                    dm.set_max_temp_directory_size(new_limit).unwrap();
947                    // Read should return SOME value set by one of the threads
948                    let current = dm.max_temp_directory_size();
949                    assert!((1000..=8000).contains(&current));
950                })
951            })
952            .collect();
953
954        for h in handles {
955            h.join().unwrap();
956        }
957
958        // Final value should be one of the values set by threads
959        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        // Setting non-zero limit on disabled DiskManager should error
992        let result = dm.set_max_temp_directory_size(1024);
993        assert!(result.is_err());
994
995        // Setting zero is OK
996        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        // Scenario: DiskManager has 100GB limit, currently using 80GB.
1004        // Admin lowers limit to 60GB. What happens?
1005        //
1006        // Expected behavior:
1007        // - Existing spill files remain on disk (not deleted)
1008        // - used_disk_space still reports 80GB
1009        // - New spill writes FAIL immediately (80GB > 60GB new limit)
1010        // - Once old queries complete and release their files (used drops below 60GB),
1011        //   new spill writes succeed again
1012        //
1013        // This demonstrates graceful degradation: lowering the limit doesn't
1014        // reclaim existing files (would break running queries), but prevents
1015        // additional spilling until usage drops naturally.
1016        let dm = DiskManager::builder()
1017            .with_max_temp_directory_size(100 * 1024 * 1024 * 1024) // 100GB
1018            .build()?;
1019        let dm = Arc::new(dm);
1020
1021        // Simulate 80GB of existing spill usage
1022        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        // Lower the limit to 60GB (below current usage)
1029        dm.set_max_temp_directory_size(60 * 1024 * 1024 * 1024)?;
1030        assert_eq!(dm.max_temp_directory_size(), 60 * 1024 * 1024 * 1024);
1031
1032        // Current usage (80GB) now exceeds the new limit (60GB).
1033        // The used_disk_space is NOT reclaimed — existing files stay.
1034        assert_eq!(dm.used_disk_space(), 80 * 1024 * 1024 * 1024);
1035
1036        // Any attempt to write MORE would be rejected at the SpillWriter level
1037        // because used_disk_space(80GB) > max_temp_directory_size(60GB).
1038        // (SpillWriter check: `global_disk_usage > limit` returns ResourcesExhausted)
1039
1040        // Simulate old queries completing: usage drops to 50GB
1041        dm.used_disk_space
1042            .store(50 * 1024 * 1024 * 1024, Ordering::Relaxed);
1043
1044        // Now usage (50GB) < limit (60GB) — new spill writes would succeed again
1045        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        // Scenario: Multiple threads spilling while limit is lowered concurrently.
1053        // Demonstrates that:
1054        // 1. In-flight spills that started before the limit change complete normally
1055        //    (they already incremented used_disk_space)
1056        // 2. New spills after the limit change respect the new lower limit
1057        // 3. No data corruption or panics from concurrent access
1058        let dm = Arc::new(
1059            DiskManager::builder()
1060                .with_max_temp_directory_size(100 * 1024 * 1024) // 100MB
1061                .build()?,
1062        );
1063
1064        let barrier = Arc::new(std::sync::Barrier::new(5));
1065
1066        // 4 threads simulate concurrent spilling
1067        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                    // Simulate spill: increment used_disk_space
1074                    dm.used_disk_space
1075                        .fetch_add(10 * 1024 * 1024, Ordering::Relaxed);
1076                    std::thread::sleep(std::time::Duration::from_millis(10));
1077                    // Simulate cleanup
1078                    dm.used_disk_space
1079                        .fetch_sub(10 * 1024 * 1024, Ordering::Relaxed);
1080                })
1081            })
1082            .collect();
1083
1084        // 1 thread lowers the limit mid-flight
1085        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            // Lower limit while spills are in progress
1090            dm_resize
1091                .set_max_temp_directory_size(30 * 1024 * 1024) // 30MB
1092                .unwrap();
1093        });
1094
1095        for h in spill_handles {
1096            h.join().unwrap();
1097        }
1098        resize_handle.join().unwrap();
1099
1100        // After all threads complete:
1101        // - Limit is 30MB (last set by resize thread)
1102        // - used_disk_space is 0 (all spills cleaned up)
1103        // - No panics, no corruption
1104        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        // This test verifies that lowering the limit, failing a spill write,
1113        // and then dropping the file leaves used_disk_space at zero.
1114        //
1115        // Without the rollback fix, the global counter would be permanently
1116        // inflated by the delta between the new and old file sizes.
1117
1118        let dm = Arc::new(
1119            DiskManager::builder()
1120                .with_max_temp_directory_size(10 * 1024 * 1024) // 10MB
1121                .build()?,
1122        );
1123
1124        let file = dm.create_tmp_file("test_rollback")?;
1125
1126        let mut writer = file.open_writer()?;
1127
1128        // Create a temp file and write some data
1129        {
1130            let data = vec![0u8; 1024]; // 1KB
1131            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        // Write more data to grow the file
1138        {
1139            let data = vec![0u8; 4 * 1024]; // 4KB more
1140            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        // Now lower the limit to 1 byte — below current usage
1147        dm.set_max_temp_directory_size(1)?;
1148
1149        // Write even more data
1150        {
1151            let data = vec![0u8; 2 * 1024]; // 2KB more
1152
1153            // This write should FAIL (exceeds new 1-byte limit)
1154            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        // Critical check: used_disk_space should still equal the LAST
1166        // successful update (before the failed one), not be inflated
1167        assert_eq!(dm.used_disk_space(), usage_after_second_write);
1168
1169        // Drop the writer and file — should subtract the last successful file size
1170        drop(writer);
1171        drop(file);
1172
1173        // After drop: used_disk_space must be zero (no leak)
1174        assert_eq!(dm.used_disk_space(), 0);
1175
1176        Ok(())
1177    }
1178}