@@ -75,7 +75,7 @@ impl DiskManagerBuilder {
7575 match self . mode {
7676 DiskManagerMode :: OsTmpDirectory => Ok ( DiskManager {
7777 local_dirs : Mutex :: new ( Some ( vec ! [ ] ) ) ,
78- max_temp_directory_size : self . max_temp_directory_size ,
78+ max_temp_directory_size : AtomicU64 :: new ( self . max_temp_directory_size ) ,
7979 used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
8080 active_files_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
8181 } ) ,
@@ -86,14 +86,14 @@ impl DiskManagerBuilder {
8686 ) ;
8787 Ok ( DiskManager {
8888 local_dirs : Mutex :: new ( Some ( local_dirs) ) ,
89- max_temp_directory_size : self . max_temp_directory_size ,
89+ max_temp_directory_size : AtomicU64 :: new ( self . max_temp_directory_size ) ,
9090 used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
9191 active_files_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
9292 } )
9393 }
9494 DiskManagerMode :: Disabled => Ok ( DiskManager {
9595 local_dirs : Mutex :: new ( None ) ,
96- max_temp_directory_size : self . max_temp_directory_size ,
96+ max_temp_directory_size : AtomicU64 :: new ( self . max_temp_directory_size ) ,
9797 used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
9898 active_files_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
9999 } ) ,
@@ -167,8 +167,9 @@ pub struct DiskManager {
167167 /// If `None` an error will be returned (configured not to spill)
168168 local_dirs : Mutex < Option < Vec < Arc < TempDir > > > > ,
169169 /// The maximum amount of data (in bytes) stored inside the temporary directories.
170- /// Default to 100GB
171- max_temp_directory_size : u64 ,
170+ /// Default to 100GB. Stored as `AtomicU64` so it can be adjusted at runtime
171+ /// without requiring exclusive (`&mut`) access to the `DiskManager`.
172+ max_temp_directory_size : AtomicU64 ,
172173 /// Used disk space in the temporary directories. Now only spilled data for
173174 /// external executors are counted.
174175 used_disk_space : Arc < AtomicU64 > ,
@@ -199,7 +200,7 @@ impl DiskManager {
199200 DiskManagerConfig :: Existing ( manager) => Ok ( manager) ,
200201 DiskManagerConfig :: NewOs => Ok ( Arc :: new ( Self {
201202 local_dirs : Mutex :: new ( Some ( vec ! [ ] ) ) ,
202- max_temp_directory_size : DEFAULT_MAX_TEMP_DIRECTORY_SIZE ,
203+ max_temp_directory_size : AtomicU64 :: new ( DEFAULT_MAX_TEMP_DIRECTORY_SIZE ) ,
203204 used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
204205 active_files_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
205206 } ) ) ,
@@ -210,53 +211,73 @@ impl DiskManager {
210211 ) ;
211212 Ok ( Arc :: new ( Self {
212213 local_dirs : Mutex :: new ( Some ( local_dirs) ) ,
213- max_temp_directory_size : DEFAULT_MAX_TEMP_DIRECTORY_SIZE ,
214+ max_temp_directory_size : AtomicU64 :: new ( DEFAULT_MAX_TEMP_DIRECTORY_SIZE ) ,
214215 used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
215216 active_files_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
216217 } ) )
217218 }
218219 DiskManagerConfig :: Disabled => Ok ( Arc :: new ( Self {
219220 local_dirs : Mutex :: new ( None ) ,
220- max_temp_directory_size : DEFAULT_MAX_TEMP_DIRECTORY_SIZE ,
221+ max_temp_directory_size : AtomicU64 :: new ( DEFAULT_MAX_TEMP_DIRECTORY_SIZE ) ,
221222 used_disk_space : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
222223 active_files_count : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
223224 } ) ) ,
224225 }
225226 }
226227
228+ /// Set the max temp directory size. Requires exclusive access.
229+ ///
230+ /// Prefer [`Self::update_max_temp_directory_size`] which takes `&self` and
231+ /// works through `Arc` without exclusive access.
232+ #[ deprecated(
233+ since = "54.0.0" ,
234+ note = "Use `update_max_temp_directory_size` instead, which takes &self and works through Arc."
235+ ) ]
227236 pub fn set_max_temp_directory_size (
228237 & mut self ,
229238 max_temp_directory_size : u64 ,
230239 ) -> Result < ( ) > {
231- // If the disk manager is disabled and `max_temp_directory_size` is not 0,
232- // this operation is not meaningful, fail early.
240+ self . update_max_temp_directory_size ( max_temp_directory_size)
241+ }
242+
243+ /// Atomically update the max temp directory size at runtime.
244+ ///
245+ /// Takes `&self` (not `&mut self`), so it works through `Arc<DiskManager>`
246+ /// without requiring exclusive access. Takes effect immediately for
247+ /// subsequent spill writes.
248+ ///
249+ /// Use this when you need to adjust the limit dynamically while queries
250+ /// are running (e.g., adapting to available disk space).
251+ pub fn update_max_temp_directory_size (
252+ & self ,
253+ max_temp_directory_size : u64 ,
254+ ) -> Result < ( ) > {
233255 if self . local_dirs . lock ( ) . is_none ( ) && max_temp_directory_size != 0 {
234256 return config_err ! (
235257 "Cannot set max temp directory size for a disk manager that spilling is disabled"
236258 ) ;
237259 }
238260
239- self . max_temp_directory_size = max_temp_directory_size;
261+ self . max_temp_directory_size . store ( max_temp_directory_size, Ordering :: Release ) ;
240262 Ok ( ( ) )
241263 }
242264
265+ #[ deprecated(
266+ since = "54.0.0" ,
267+ note = "Use `update_max_temp_directory_size` instead"
268+ ) ]
243269 pub fn set_arc_max_temp_directory_size (
244- this : & mut Arc < Self > ,
270+ this : & Arc < Self > ,
245271 max_temp_directory_size : u64 ,
246272 ) -> Result < ( ) > {
247- if let Some ( inner) = Arc :: get_mut ( this) {
248- inner. set_max_temp_directory_size ( max_temp_directory_size) ?;
249- Ok ( ( ) )
250- } else {
251- config_err ! ( "DiskManager should be a single instance" )
252- }
273+ this. update_max_temp_directory_size ( max_temp_directory_size)
253274 }
254275
255276 pub fn with_max_temp_directory_size (
256277 mut self ,
257278 max_temp_directory_size : u64 ,
258279 ) -> Result < Self > {
259- self . set_max_temp_directory_size ( max_temp_directory_size) ?;
280+ self . update_max_temp_directory_size ( max_temp_directory_size) ?;
260281 Ok ( self )
261282 }
262283
@@ -266,7 +287,7 @@ impl DiskManager {
266287
267288 /// Returns the maximum temporary directory size in bytes
268289 pub fn max_temp_directory_size ( & self ) -> u64 {
269- self . max_temp_directory_size
290+ self . max_temp_directory_size . load ( Ordering :: Acquire )
270291 }
271292
272293 /// Returns the current spilling progress
@@ -418,11 +439,12 @@ impl RefCountedTempFile {
418439
419440 // 3. Check if the updated global disk usage exceeds the configured limit
420441 let global_disk_usage = self . disk_manager . used_disk_space . load ( Ordering :: Relaxed ) ;
421- if global_disk_usage > self . disk_manager . max_temp_directory_size {
442+ let limit = self . disk_manager . max_temp_directory_size . load ( Ordering :: Acquire ) ;
443+ if global_disk_usage > limit {
422444 return resources_err ! (
423445 "The used disk space during the spilling process has exceeded the allowable limit of {}. \
424446 Please try increasing the config: `datafusion.runtime.max_temp_directory_size`.",
425- human_readable_size( self . disk_manager . max_temp_directory_size as usize )
447+ human_readable_size( limit as usize )
426448 ) ;
427449 }
428450
@@ -796,4 +818,183 @@ mod tests {
796818
797819 Ok ( ( ) )
798820 }
821+
822+ #[ test]
823+ fn test_dynamic_limit_adjustment_through_shared_ref ( ) -> Result < ( ) > {
824+ // Verify that set_max_temp_directory_size works through &self (not &mut self).
825+ // This is the key behavioral change: the limit can be adjusted at runtime
826+ // without exclusive access, enabling dynamic resize while queries are running.
827+ let dm = DiskManager :: builder ( )
828+ . with_max_temp_directory_size ( 1024 )
829+ . build ( ) ?;
830+ let dm = Arc :: new ( dm) ;
831+
832+ assert_eq ! ( dm. max_temp_directory_size( ) , 1024 ) ;
833+
834+ // Adjust through shared reference (simulates concurrent access via Arc)
835+ dm. update_max_temp_directory_size ( 2048 ) ?;
836+ assert_eq ! ( dm. max_temp_directory_size( ) , 2048 ) ;
837+
838+ // Can also decrease
839+ dm. update_max_temp_directory_size ( 512 ) ?;
840+ assert_eq ! ( dm. max_temp_directory_size( ) , 512 ) ;
841+
842+ Ok ( ( ) )
843+ }
844+
845+ #[ test]
846+ fn test_dynamic_limit_concurrent_access ( ) -> Result < ( ) > {
847+ // Verify that multiple threads can read and write the limit concurrently
848+ let dm = Arc :: new (
849+ DiskManager :: builder ( )
850+ . with_max_temp_directory_size ( 1000 )
851+ . build ( ) ?,
852+ ) ;
853+
854+ let handles: Vec < _ > = ( 0 ..8 )
855+ . map ( |i| {
856+ let dm = Arc :: clone ( & dm) ;
857+ std:: thread:: spawn ( move || {
858+ // Each thread sets a different limit and reads it back
859+ let new_limit = ( i + 1 ) * 1000 ;
860+ dm. update_max_temp_directory_size ( new_limit) . unwrap ( ) ;
861+ // Read should return SOME value set by one of the threads
862+ let current = dm. max_temp_directory_size ( ) ;
863+ assert ! ( current >= 1000 && current <= 8000 ) ;
864+ } )
865+ } )
866+ . collect ( ) ;
867+
868+ for h in handles {
869+ h. join ( ) . unwrap ( ) ;
870+ }
871+
872+ // Final value should be one of the values set by threads
873+ let final_val = dm. max_temp_directory_size ( ) ;
874+ assert ! ( final_val >= 1000 && final_val <= 8000 ) ;
875+
876+ Ok ( ( ) )
877+ }
878+
879+ #[ test]
880+ fn test_disabled_disk_manager_rejects_nonzero_limit ( ) -> Result < ( ) > {
881+ let dm = DiskManager :: builder ( )
882+ . with_mode ( DiskManagerMode :: Disabled )
883+ . build ( ) ?;
884+ let dm = Arc :: new ( dm) ;
885+
886+ // Setting non-zero limit on disabled DiskManager should error
887+ let result = dm. update_max_temp_directory_size ( 1024 ) ;
888+ assert ! ( result. is_err( ) ) ;
889+
890+ // Setting zero is OK
891+ assert ! ( dm. update_max_temp_directory_size( 0 ) . is_ok( ) ) ;
892+
893+ Ok ( ( ) )
894+ }
895+
896+ #[ test]
897+ fn test_limit_decrease_below_current_usage ( ) -> Result < ( ) > {
898+ // Scenario: DiskManager has 100GB limit, currently using 80GB.
899+ // Admin lowers limit to 60GB. What happens?
900+ //
901+ // Expected behavior:
902+ // - Existing spill files remain on disk (not deleted)
903+ // - used_disk_space still reports 80GB
904+ // - New spill writes FAIL immediately (80GB > 60GB new limit)
905+ // - Once old queries complete and release their files (used drops below 60GB),
906+ // new spill writes succeed again
907+ //
908+ // This demonstrates graceful degradation: lowering the limit doesn't
909+ // reclaim existing files (would break running queries), but prevents
910+ // additional spilling until usage drops naturally.
911+ let dm = DiskManager :: builder ( )
912+ . with_max_temp_directory_size ( 100 * 1024 * 1024 * 1024 ) // 100GB
913+ . build ( ) ?;
914+ let dm = Arc :: new ( dm) ;
915+
916+ // Simulate 80GB of existing spill usage
917+ dm. used_disk_space . store ( 80 * 1024 * 1024 * 1024 , Ordering :: Relaxed ) ;
918+
919+ assert_eq ! ( dm. max_temp_directory_size( ) , 100 * 1024 * 1024 * 1024 ) ;
920+ assert_eq ! ( dm. used_disk_space( ) , 80 * 1024 * 1024 * 1024 ) ;
921+
922+ // Lower the limit to 60GB (below current usage)
923+ dm. update_max_temp_directory_size ( 60 * 1024 * 1024 * 1024 ) ?;
924+ assert_eq ! ( dm. max_temp_directory_size( ) , 60 * 1024 * 1024 * 1024 ) ;
925+
926+ // Current usage (80GB) now exceeds the new limit (60GB).
927+ // The used_disk_space is NOT reclaimed — existing files stay.
928+ assert_eq ! ( dm. used_disk_space( ) , 80 * 1024 * 1024 * 1024 ) ;
929+
930+ // Any attempt to write MORE would be rejected at the SpillWriter level
931+ // because used_disk_space(80GB) > max_temp_directory_size(60GB).
932+ // (SpillWriter check: `global_disk_usage > limit` returns ResourcesExhausted)
933+
934+ // Simulate old queries completing: usage drops to 50GB
935+ dm. used_disk_space . store ( 50 * 1024 * 1024 * 1024 , Ordering :: Relaxed ) ;
936+
937+ // Now usage (50GB) < limit (60GB) — new spill writes would succeed again
938+ assert ! ( dm. used_disk_space( ) < dm. max_temp_directory_size( ) ) ;
939+
940+ Ok ( ( ) )
941+ }
942+
943+ #[ test]
944+ fn test_limit_decrease_with_concurrent_queries ( ) -> Result < ( ) > {
945+ // Scenario: Multiple threads spilling while limit is lowered concurrently.
946+ // Demonstrates that:
947+ // 1. In-flight spills that started before the limit change complete normally
948+ // (they already incremented used_disk_space)
949+ // 2. New spills after the limit change respect the new lower limit
950+ // 3. No data corruption or panics from concurrent access
951+ let dm = Arc :: new (
952+ DiskManager :: builder ( )
953+ . with_max_temp_directory_size ( 100 * 1024 * 1024 ) // 100MB
954+ . build ( ) ?,
955+ ) ;
956+
957+ let barrier = Arc :: new ( std:: sync:: Barrier :: new ( 5 ) ) ;
958+
959+ // 4 threads simulate concurrent spilling
960+ let spill_handles: Vec < _ > = ( 0 ..4 )
961+ . map ( |_| {
962+ let dm = Arc :: clone ( & dm) ;
963+ let barrier = Arc :: clone ( & barrier) ;
964+ std:: thread:: spawn ( move || {
965+ barrier. wait ( ) ;
966+ // Simulate spill: increment used_disk_space
967+ dm. used_disk_space . fetch_add ( 10 * 1024 * 1024 , Ordering :: Relaxed ) ;
968+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 10 ) ) ;
969+ // Simulate cleanup
970+ dm. used_disk_space . fetch_sub ( 10 * 1024 * 1024 , Ordering :: Relaxed ) ;
971+ } )
972+ } )
973+ . collect ( ) ;
974+
975+ // 1 thread lowers the limit mid-flight
976+ let dm_resize = Arc :: clone ( & dm) ;
977+ let resize_barrier = Arc :: clone ( & barrier) ;
978+ let resize_handle = std:: thread:: spawn ( move || {
979+ resize_barrier. wait ( ) ;
980+ // Lower limit while spills are in progress
981+ dm_resize
982+ . update_max_temp_directory_size ( 30 * 1024 * 1024 ) // 30MB
983+ . unwrap ( ) ;
984+ } ) ;
985+
986+ for h in spill_handles {
987+ h. join ( ) . unwrap ( ) ;
988+ }
989+ resize_handle. join ( ) . unwrap ( ) ;
990+
991+ // After all threads complete:
992+ // - Limit is 30MB (last set by resize thread)
993+ // - used_disk_space is 0 (all spills cleaned up)
994+ // - No panics, no corruption
995+ assert_eq ! ( dm. max_temp_directory_size( ) , 30 * 1024 * 1024 ) ;
996+ assert_eq ! ( dm. used_disk_space( ) , 0 ) ;
997+
998+ Ok ( ( ) )
999+ }
7991000}
0 commit comments