11import datetime
22from typing import List
33
4- from sqlalchemy import select , update
4+ from sqlalchemy import select
55from sqlalchemy .ext .asyncio import AsyncSession
66from sqlalchemy .orm import joinedload
77
@@ -37,15 +37,14 @@ async def process_idle_volumes():
3737 )
3838 .order_by (VolumeModel .last_processed_at .asc ())
3939 .limit (10 )
40- .with_for_update (skip_locked = True )
40+ .with_for_update (skip_locked = True , key_share = True )
4141 )
4242 volume_ids = list (res .scalars ().all ())
4343 if not volume_ids :
4444 return
4545 for volume_id in volume_ids :
4646 lockset .add (volume_id )
4747
48- # Refetch volumes with proper relationship loading to avoid MissingGreenlet
4948 res = await session .execute (
5049 select (VolumeModel )
5150 .where (VolumeModel .id .in_ (volume_ids ))
@@ -54,89 +53,65 @@ async def process_idle_volumes():
5453 .options (joinedload (VolumeModel .attachments ))
5554 .execution_options (populate_existing = True )
5655 )
57- volumes = list (res .unique ().scalars ().all ())
58-
56+ volume_models = list (res .unique ().scalars ().all ())
5957 try :
60- to_delete = []
61- for volume in volumes :
62- if _should_delete_volume (volume ):
63- to_delete .append (volume )
64-
65- if to_delete :
66- await _delete_idle_volumes (session , to_delete )
67-
58+ volumes_to_delete = [v for v in volume_models if _should_delete_volume (v )]
59+ if not volumes_to_delete :
60+ return
61+ await _delete_idle_volumes (session , volumes_to_delete )
6862 finally :
6963 lockset .difference_update (volume_ids )
7064
7165
7266def _should_delete_volume (volume : VolumeModel ) -> bool :
73- config = get_volume_configuration (volume )
74-
75- if not config .auto_cleanup_duration :
67+ if volume .attachments :
7668 return False
7769
78- if isinstance (config .auto_cleanup_duration , int ) and config .auto_cleanup_duration < 0 :
70+ config = get_volume_configuration (volume )
71+ if not config .auto_cleanup_duration :
7972 return False
8073
8174 duration_seconds = parse_duration (config .auto_cleanup_duration )
8275 if not duration_seconds or duration_seconds <= 0 :
8376 return False
8477
85- if volume .attachments :
86- return False
87-
8878 idle_time = _get_idle_time (volume )
8979 threshold = datetime .timedelta (seconds = duration_seconds )
90-
91- if idle_time > threshold :
92- logger .info (
93- "Deleting idle volume %s (idle %.1fh)" , volume .name , idle_time .total_seconds () / 3600
94- )
95- return True
96-
97- return False
80+ return idle_time > threshold
9881
9982
10083def _get_idle_time (volume : VolumeModel ) -> datetime .timedelta :
10184 last_used = volume .last_job_processed_at or volume .created_at
10285 last_used_utc = last_used .replace (tzinfo = datetime .timezone .utc )
103- now = get_current_datetime ()
104-
105- idle_time = now - last_used_utc
86+ idle_time = get_current_datetime () - last_used_utc
10687 return max (idle_time , datetime .timedelta (0 ))
10788
10889
10990async def _delete_idle_volumes (session : AsyncSession , volumes : List [VolumeModel ]):
110- """Delete idle volumes from cloud providers and mark as deleted in database."""
91+ # Note: Multiple volumes are deleted in the same transaction,
92+ # so long deletion of one volume may block processing other volumes.
11193 for volume_model in volumes :
94+ logger .info ("Deleting idle volume %s" , volume_model .name )
11295 try :
113- await _delete_volume_from_cloud (session , volume_model )
114- except Exception :
115- logger .exception ("Error when deleting volume %s from cloud" , volume_model .name )
116- try :
117- await session .execute (
118- update (VolumeModel )
119- .where (VolumeModel .id == volume_model .id )
120- .values (
121- deleted = True ,
122- deleted_at = get_current_datetime (),
123- )
124- )
125- logger .info ("Deleted idle volume %s" , volume_model .name )
96+ await _delete_idle_volume (session , volume_model )
12697 except Exception :
127- logger .exception ("Failed to mark volume %s as deleted in database" , volume_model .name )
98+ logger .exception ("Error when deleting idle volume %s" , volume_model .name )
99+
100+ volume_model .deleted = True
101+ volume_model .deleted_at = get_current_datetime ()
102+
103+ logger .info ("Deleted idle volume %s" , volume_model .name )
128104
129105 await session .commit ()
130106
131107
132- async def _delete_volume_from_cloud (session : AsyncSession , volume_model : VolumeModel ):
133- """Delete volume from cloud provider. Based on volumes.py:_delete_volume"""
108+ async def _delete_idle_volume (session : AsyncSession , volume_model : VolumeModel ):
134109 volume = volume_model_to_volume (volume_model )
135110
136- if volume .external :
137- return
138-
139111 if volume .provisioning_data is None :
112+ logger .error (
113+ f"Failed to delete volume { volume_model .name } . volume.provisioning_data is None."
114+ )
140115 return
141116
142117 if volume .provisioning_data .backend is None :
0 commit comments