@@ -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,70 @@ 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( note = "Use `update_max_temp_directory_size` instead" ) ]
243266 pub fn set_arc_max_temp_directory_size (
244- this : & mut Arc < Self > ,
267+ this : & Arc < Self > ,
245268 max_temp_directory_size : u64 ,
246269 ) -> 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- }
270+ this. update_max_temp_directory_size ( max_temp_directory_size)
253271 }
254272
255273 pub fn with_max_temp_directory_size (
256274 mut self ,
257275 max_temp_directory_size : u64 ,
258276 ) -> Result < Self > {
259- self . set_max_temp_directory_size ( max_temp_directory_size) ?;
277+ self . update_max_temp_directory_size ( max_temp_directory_size) ?;
260278 Ok ( self )
261279 }
262280
@@ -266,7 +284,7 @@ impl DiskManager {
266284
267285 /// Returns the maximum temporary directory size in bytes
268286 pub fn max_temp_directory_size ( & self ) -> u64 {
269- self . max_temp_directory_size
287+ self . max_temp_directory_size . load ( Ordering :: Acquire )
270288 }
271289
272290 /// Returns the current spilling progress
@@ -418,11 +436,12 @@ impl RefCountedTempFile {
418436
419437 // 3. Check if the updated global disk usage exceeds the configured limit
420438 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 {
439+ let limit = self . disk_manager . max_temp_directory_size . load ( Ordering :: Acquire ) ;
440+ if global_disk_usage > limit {
422441 return resources_err ! (
423442 "The used disk space during the spilling process has exceeded the allowable limit of {}. \
424443 Please try increasing the config: `datafusion.runtime.max_temp_directory_size`.",
425- human_readable_size( self . disk_manager . max_temp_directory_size as usize )
444+ human_readable_size( limit as usize )
426445 ) ;
427446 }
428447
@@ -796,4 +815,183 @@ mod tests {
796815
797816 Ok ( ( ) )
798817 }
818+
819+ #[ test]
820+ fn test_dynamic_limit_adjustment_through_shared_ref ( ) -> Result < ( ) > {
821+ // Verify that set_max_temp_directory_size works through &self (not &mut self).
822+ // This is the key behavioral change: the limit can be adjusted at runtime
823+ // without exclusive access, enabling dynamic resize while queries are running.
824+ let dm = DiskManager :: builder ( )
825+ . with_max_temp_directory_size ( 1024 )
826+ . build ( ) ?;
827+ let dm = Arc :: new ( dm) ;
828+
829+ assert_eq ! ( dm. max_temp_directory_size( ) , 1024 ) ;
830+
831+ // Adjust through shared reference (simulates concurrent access via Arc)
832+ dm. update_max_temp_directory_size ( 2048 ) ?;
833+ assert_eq ! ( dm. max_temp_directory_size( ) , 2048 ) ;
834+
835+ // Can also decrease
836+ dm. update_max_temp_directory_size ( 512 ) ?;
837+ assert_eq ! ( dm. max_temp_directory_size( ) , 512 ) ;
838+
839+ Ok ( ( ) )
840+ }
841+
842+ #[ test]
843+ fn test_dynamic_limit_concurrent_access ( ) -> Result < ( ) > {
844+ // Verify that multiple threads can read and write the limit concurrently
845+ let dm = Arc :: new (
846+ DiskManager :: builder ( )
847+ . with_max_temp_directory_size ( 1000 )
848+ . build ( ) ?,
849+ ) ;
850+
851+ let handles: Vec < _ > = ( 0 ..8 )
852+ . map ( |i| {
853+ let dm = Arc :: clone ( & dm) ;
854+ std:: thread:: spawn ( move || {
855+ // Each thread sets a different limit and reads it back
856+ let new_limit = ( i + 1 ) * 1000 ;
857+ dm. update_max_temp_directory_size ( new_limit) . unwrap ( ) ;
858+ // Read should return SOME value set by one of the threads
859+ let current = dm. max_temp_directory_size ( ) ;
860+ assert ! ( current >= 1000 && current <= 8000 ) ;
861+ } )
862+ } )
863+ . collect ( ) ;
864+
865+ for h in handles {
866+ h. join ( ) . unwrap ( ) ;
867+ }
868+
869+ // Final value should be one of the values set by threads
870+ let final_val = dm. max_temp_directory_size ( ) ;
871+ assert ! ( final_val >= 1000 && final_val <= 8000 ) ;
872+
873+ Ok ( ( ) )
874+ }
875+
876+ #[ test]
877+ fn test_disabled_disk_manager_rejects_nonzero_limit ( ) -> Result < ( ) > {
878+ let dm = DiskManager :: builder ( )
879+ . with_mode ( DiskManagerMode :: Disabled )
880+ . build ( ) ?;
881+ let dm = Arc :: new ( dm) ;
882+
883+ // Setting non-zero limit on disabled DiskManager should error
884+ let result = dm. update_max_temp_directory_size ( 1024 ) ;
885+ assert ! ( result. is_err( ) ) ;
886+
887+ // Setting zero is OK
888+ assert ! ( dm. update_max_temp_directory_size( 0 ) . is_ok( ) ) ;
889+
890+ Ok ( ( ) )
891+ }
892+
893+ #[ test]
894+ fn test_limit_decrease_below_current_usage ( ) -> Result < ( ) > {
895+ // Scenario: DiskManager has 100GB limit, currently using 80GB.
896+ // Admin lowers limit to 60GB. What happens?
897+ //
898+ // Expected behavior:
899+ // - Existing spill files remain on disk (not deleted)
900+ // - used_disk_space still reports 80GB
901+ // - New spill writes FAIL immediately (80GB > 60GB new limit)
902+ // - Once old queries complete and release their files (used drops below 60GB),
903+ // new spill writes succeed again
904+ //
905+ // This demonstrates graceful degradation: lowering the limit doesn't
906+ // reclaim existing files (would break running queries), but prevents
907+ // additional spilling until usage drops naturally.
908+ let dm = DiskManager :: builder ( )
909+ . with_max_temp_directory_size ( 100 * 1024 * 1024 * 1024 ) // 100GB
910+ . build ( ) ?;
911+ let dm = Arc :: new ( dm) ;
912+
913+ // Simulate 80GB of existing spill usage
914+ dm. used_disk_space . store ( 80 * 1024 * 1024 * 1024 , Ordering :: Relaxed ) ;
915+
916+ assert_eq ! ( dm. max_temp_directory_size( ) , 100 * 1024 * 1024 * 1024 ) ;
917+ assert_eq ! ( dm. used_disk_space( ) , 80 * 1024 * 1024 * 1024 ) ;
918+
919+ // Lower the limit to 60GB (below current usage)
920+ dm. update_max_temp_directory_size ( 60 * 1024 * 1024 * 1024 ) ?;
921+ assert_eq ! ( dm. max_temp_directory_size( ) , 60 * 1024 * 1024 * 1024 ) ;
922+
923+ // Current usage (80GB) now exceeds the new limit (60GB).
924+ // The used_disk_space is NOT reclaimed — existing files stay.
925+ assert_eq ! ( dm. used_disk_space( ) , 80 * 1024 * 1024 * 1024 ) ;
926+
927+ // Any attempt to write MORE would be rejected at the SpillWriter level
928+ // because used_disk_space(80GB) > max_temp_directory_size(60GB).
929+ // (SpillWriter check: `global_disk_usage > limit` returns ResourcesExhausted)
930+
931+ // Simulate old queries completing: usage drops to 50GB
932+ dm. used_disk_space . store ( 50 * 1024 * 1024 * 1024 , Ordering :: Relaxed ) ;
933+
934+ // Now usage (50GB) < limit (60GB) — new spill writes would succeed again
935+ assert ! ( dm. used_disk_space( ) < dm. max_temp_directory_size( ) ) ;
936+
937+ Ok ( ( ) )
938+ }
939+
940+ #[ test]
941+ fn test_limit_decrease_with_concurrent_queries ( ) -> Result < ( ) > {
942+ // Scenario: Multiple threads spilling while limit is lowered concurrently.
943+ // Demonstrates that:
944+ // 1. In-flight spills that started before the limit change complete normally
945+ // (they already incremented used_disk_space)
946+ // 2. New spills after the limit change respect the new lower limit
947+ // 3. No data corruption or panics from concurrent access
948+ let dm = Arc :: new (
949+ DiskManager :: builder ( )
950+ . with_max_temp_directory_size ( 100 * 1024 * 1024 ) // 100MB
951+ . build ( ) ?,
952+ ) ;
953+
954+ let barrier = Arc :: new ( std:: sync:: Barrier :: new ( 5 ) ) ;
955+
956+ // 4 threads simulate concurrent spilling
957+ let spill_handles: Vec < _ > = ( 0 ..4 )
958+ . map ( |_| {
959+ let dm = Arc :: clone ( & dm) ;
960+ let barrier = Arc :: clone ( & barrier) ;
961+ std:: thread:: spawn ( move || {
962+ barrier. wait ( ) ;
963+ // Simulate spill: increment used_disk_space
964+ dm. used_disk_space . fetch_add ( 10 * 1024 * 1024 , Ordering :: Relaxed ) ;
965+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 10 ) ) ;
966+ // Simulate cleanup
967+ dm. used_disk_space . fetch_sub ( 10 * 1024 * 1024 , Ordering :: Relaxed ) ;
968+ } )
969+ } )
970+ . collect ( ) ;
971+
972+ // 1 thread lowers the limit mid-flight
973+ let dm_resize = Arc :: clone ( & dm) ;
974+ let resize_barrier = Arc :: clone ( & barrier) ;
975+ let resize_handle = std:: thread:: spawn ( move || {
976+ resize_barrier. wait ( ) ;
977+ // Lower limit while spills are in progress
978+ dm_resize
979+ . update_max_temp_directory_size ( 30 * 1024 * 1024 ) // 30MB
980+ . unwrap ( ) ;
981+ } ) ;
982+
983+ for h in spill_handles {
984+ h. join ( ) . unwrap ( ) ;
985+ }
986+ resize_handle. join ( ) . unwrap ( ) ;
987+
988+ // After all threads complete:
989+ // - Limit is 30MB (last set by resize thread)
990+ // - used_disk_space is 0 (all spills cleaned up)
991+ // - No panics, no corruption
992+ assert_eq ! ( dm. max_temp_directory_size( ) , 30 * 1024 * 1024 ) ;
993+ assert_eq ! ( dm. used_disk_space( ) , 0 ) ;
994+
995+ Ok ( ( ) )
996+ }
799997}
0 commit comments