17
17
18
18
//! [`DiskManager`]: Manages files generated during query execution
19
19
20
- use datafusion_common:: { resources_datafusion_err, DataFusionError , Result } ;
20
+ use datafusion_common:: {
21
+ config_err, resources_datafusion_err, resources_err, DataFusionError , Result ,
22
+ } ;
21
23
use log:: debug;
22
24
use parking_lot:: Mutex ;
23
25
use rand:: { thread_rng, Rng } ;
24
26
use std:: path:: { Path , PathBuf } ;
27
+ use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
25
28
use std:: sync:: Arc ;
26
29
use tempfile:: { Builder , NamedTempFile , TempDir } ;
27
30
31
+ use crate :: memory_pool:: human_readable_size;
32
+
33
+ const DEFAULT_MAX_TEMP_DIRECTORY_SIZE : u64 = 100 * 1024 * 1024 * 1024 ; // 100GB
34
+
28
35
/// Configuration for temporary disk access
29
36
#[ derive( Debug , Clone ) ]
30
37
pub enum DiskManagerConfig {
@@ -75,6 +82,12 @@ pub struct DiskManager {
75
82
/// If `Some(vec![])` a new OS specified temporary directory will be created
76
83
/// If `None` an error will be returned (configured not to spill)
77
84
local_dirs : Mutex < Option < Vec < Arc < TempDir > > > > ,
85
+ /// The maximum amount of data (in bytes) stored inside the temporary directories.
86
+ /// Default to 100GB
87
+ max_temp_directory_size : u64 ,
88
+ /// Used disk space in the temporary directories. Now only spilled data for
89
+ /// external executors are counted.
90
+ used_disk_space : Arc < AtomicU64 > ,
78
91
}
79
92
80
93
impl DiskManager {
@@ -84,6 +97,8 @@ impl DiskManager {
84
97
DiskManagerConfig :: Existing ( manager) => Ok ( manager) ,
85
98
DiskManagerConfig :: NewOs => Ok ( Arc :: new ( Self {
86
99
local_dirs : Mutex :: new ( Some ( vec ! [ ] ) ) ,
100
+ max_temp_directory_size : DEFAULT_MAX_TEMP_DIRECTORY_SIZE ,
101
+ used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
87
102
} ) ) ,
88
103
DiskManagerConfig :: NewSpecified ( conf_dirs) => {
89
104
let local_dirs = create_local_dirs ( conf_dirs) ?;
@@ -93,14 +108,38 @@ impl DiskManager {
93
108
) ;
94
109
Ok ( Arc :: new ( Self {
95
110
local_dirs : Mutex :: new ( Some ( local_dirs) ) ,
111
+ max_temp_directory_size : DEFAULT_MAX_TEMP_DIRECTORY_SIZE ,
112
+ used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
96
113
} ) )
97
114
}
98
115
DiskManagerConfig :: Disabled => Ok ( Arc :: new ( Self {
99
116
local_dirs : Mutex :: new ( None ) ,
117
+ max_temp_directory_size : DEFAULT_MAX_TEMP_DIRECTORY_SIZE ,
118
+ used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
100
119
} ) ) ,
101
120
}
102
121
}
103
122
123
+ pub fn with_max_temp_directory_size (
124
+ mut self ,
125
+ max_temp_directory_size : u64 ,
126
+ ) -> Result < Self > {
127
+ // If the disk manager is disabled and `max_temp_directory_size` is not 0,
128
+ // this operation is not meaningful, fail early.
129
+ if self . local_dirs . lock ( ) . is_none ( ) && max_temp_directory_size != 0 {
130
+ return config_err ! (
131
+ "Cannot set max temp directory size for a disk manager that spilling is disabled"
132
+ ) ;
133
+ }
134
+
135
+ self . max_temp_directory_size = max_temp_directory_size;
136
+ Ok ( self )
137
+ }
138
+
139
+ pub fn used_disk_space ( & self ) -> u64 {
140
+ self . used_disk_space . load ( Ordering :: Relaxed )
141
+ }
142
+
104
143
/// Return true if this disk manager supports creating temporary
105
144
/// files. If this returns false, any call to `create_tmp_file`
106
145
/// will error.
@@ -113,7 +152,7 @@ impl DiskManager {
113
152
/// If the file can not be created for some reason, returns an
114
153
/// error message referencing the request description
115
154
pub fn create_tmp_file (
116
- & self ,
155
+ self : & Arc < Self > ,
117
156
request_description : & str ,
118
157
) -> Result < RefCountedTempFile > {
119
158
let mut guard = self . local_dirs . lock ( ) ;
@@ -142,18 +181,31 @@ impl DiskManager {
142
181
tempfile : Builder :: new ( )
143
182
. tempfile_in ( local_dirs[ dir_index] . as_ref ( ) )
144
183
. map_err ( DataFusionError :: IoError ) ?,
184
+ current_file_disk_usage : 0 ,
185
+ disk_manager : Arc :: clone ( self ) ,
145
186
} )
146
187
}
147
188
}
148
189
149
190
/// A wrapper around a [`NamedTempFile`] that also contains
150
- /// a reference to its parent temporary directory
191
+ /// a reference to its parent temporary directory.
192
+ ///
193
+ /// # Note
194
+ /// After any modification to the underlying file (e.g., writing data to it), the caller
195
+ /// must invoke [`Self::update_disk_usage`] to update the global disk usage counter.
196
+ /// This ensures the disk manager can properly enforce usage limits configured by
197
+ /// [`DiskManager::with_max_temp_directory_size`].
151
198
#[ derive( Debug ) ]
152
199
pub struct RefCountedTempFile {
153
200
/// The reference to the directory in which temporary files are created to ensure
154
201
/// it is not cleaned up prior to the NamedTempFile
155
202
_parent_temp_dir : Arc < TempDir > ,
156
203
tempfile : NamedTempFile ,
204
+ /// Tracks the current disk usage of this temporary file. See
205
+ /// [`Self::update_disk_usage`] for more details.
206
+ current_file_disk_usage : u64 ,
207
+ /// The disk manager that created and manages this temporary file
208
+ disk_manager : Arc < DiskManager > ,
157
209
}
158
210
159
211
impl RefCountedTempFile {
@@ -164,6 +216,50 @@ impl RefCountedTempFile {
164
216
pub fn inner ( & self ) -> & NamedTempFile {
165
217
& self . tempfile
166
218
}
219
+
220
+ /// Updates the global disk usage counter after modifications to the underlying file.
221
+ ///
222
+ /// # Errors
223
+ /// - Returns an error if the global disk usage exceeds the configured limit.
224
+ pub fn update_disk_usage ( & mut self ) -> Result < ( ) > {
225
+ // Get new file size from OS
226
+ let metadata = self . tempfile . as_file ( ) . metadata ( ) ?;
227
+ let new_disk_usage = metadata. len ( ) ;
228
+
229
+ // Update the global disk usage by:
230
+ // 1. Subtracting the old file size from the global counter
231
+ self . disk_manager
232
+ . used_disk_space
233
+ . fetch_sub ( self . current_file_disk_usage , Ordering :: Relaxed ) ;
234
+ // 2. Adding the new file size to the global counter
235
+ self . disk_manager
236
+ . used_disk_space
237
+ . fetch_add ( new_disk_usage, Ordering :: Relaxed ) ;
238
+
239
+ // 3. Check if the updated global disk usage exceeds the configured limit
240
+ let global_disk_usage = self . disk_manager . used_disk_space . load ( Ordering :: Relaxed ) ;
241
+ if global_disk_usage > self . disk_manager . max_temp_directory_size {
242
+ return resources_err ! (
243
+ "The used disk space during the spilling process has exceeded the allowable limit of {}. Try increasing the `max_temp_directory_size` in the disk manager configuration." ,
244
+ human_readable_size( self . disk_manager. max_temp_directory_size as usize )
245
+ ) ;
246
+ }
247
+
248
+ // 4. Update the local file size tracking
249
+ self . current_file_disk_usage = new_disk_usage;
250
+
251
+ Ok ( ( ) )
252
+ }
253
+ }
254
+
255
+ /// When the temporary file is dropped, subtract its disk usage from the disk manager's total
256
+ impl Drop for RefCountedTempFile {
257
+ fn drop ( & mut self ) {
258
+ // Subtract the current file's disk usage from the global counter
259
+ self . disk_manager
260
+ . used_disk_space
261
+ . fetch_sub ( self . current_file_disk_usage , Ordering :: Relaxed ) ;
262
+ }
167
263
}
168
264
169
265
/// Setup local dirs by creating one new dir in each of the given dirs
0 commit comments