11"""Background maintenance manager for periodic tasks."""
22
33import asyncio
4- from datetime import datetime , timedelta , timezone
54
65from docker import DockerClient
6+ from docker .errors import NotFound
77
88from mcp_devbench .config import get_settings
99from mcp_devbench .models .database import get_db_manager
10- from mcp_devbench .repositories .attachments import AttachmentRepository
1110from mcp_devbench .repositories .containers import ContainerRepository
1211from mcp_devbench .repositories .execs import ExecRepository
1312from mcp_devbench .utils import get_logger
13+ from mcp_devbench .utils .cleanup import cleanup_orphaned_transients
1414from mcp_devbench .utils .docker_client import get_docker_client
1515
1616logger = get_logger (__name__ )
1717
18+ # Maintenance task intervals (in seconds)
19+ MAINTENANCE_INTERVAL_SECONDS = 3600 # 1 hour
20+ MAINTENANCE_ERROR_RETRY_SECONDS = 60 # 1 minute
21+
22+ # Cleanup retention periods
23+ EXEC_RETENTION_HOURS = 24 # 24 hours
24+
1825
1926class MaintenanceManager :
2027 """Manager for background maintenance tasks."""
@@ -48,6 +55,7 @@ async def stop(self) -> None:
4855 try :
4956 await self ._task
5057 except asyncio .CancelledError :
58+ # Task cancellation is expected during shutdown
5159 pass
5260 logger .info ("Maintenance manager stopped" )
5361
@@ -57,12 +65,12 @@ async def _run_maintenance_loop(self) -> None:
5765 try :
5866 # Run maintenance tasks hourly
5967 await self .run_maintenance ()
60- await asyncio .sleep (3600 ) # 1 hour
68+ await asyncio .sleep (MAINTENANCE_INTERVAL_SECONDS )
6169 except asyncio .CancelledError :
6270 break
6371 except Exception as e :
6472 logger .error ("Maintenance task failed" , extra = {"error" : str (e )})
65- await asyncio .sleep (60 ) # Retry after 1 minute on error
73+ await asyncio .sleep (MAINTENANCE_ERROR_RETRY_SECONDS )
6674
6775 async def run_maintenance (self ) -> dict :
6876 """
@@ -119,40 +127,11 @@ async def _cleanup_orphaned_transients(self) -> int:
119127 logger .info ("Cleaning up orphaned transient containers" )
120128
121129 try :
122- cutoff_days = self .settings .transient_gc_days
123- cutoff = datetime .now (timezone .utc ) - timedelta (days = cutoff_days )
124-
125130 async with self .db_manager .get_session () as session :
126131 repo = ContainerRepository (session )
127- transients = await repo .list_by_status ("stopped" , persistent = False )
128-
129- cleaned = 0
130- for container in transients :
131- if container .last_seen < cutoff :
132- try :
133- # Try to remove Docker container if it exists
134- try :
135- docker_container = self .docker_client .containers .get (
136- container .docker_id
137- )
138- docker_container .remove (force = True )
139- except Exception :
140- pass # Container may already be gone
141-
142- # Remove from database
143- await repo .delete (container .id )
144- cleaned += 1
145-
146- logger .info (
147- "Cleaned up orphaned transient" ,
148- extra = {"container_id" : container .id },
149- )
150- except Exception as e :
151- logger .error (
152- "Failed to clean up transient" ,
153- extra = {"container_id" : container .id , "error" : str (e )},
154- )
155-
132+ cleaned = await cleanup_orphaned_transients (
133+ self .docker_client , repo , self .settings .transient_gc_days
134+ )
156135 return cleaned
157136
158137 except Exception as e :
@@ -172,8 +151,8 @@ async def _cleanup_old_execs(self) -> int:
172151 async with self .db_manager .get_session () as session :
173152 exec_repo = ExecRepository (session )
174153
175- # Clean up execs older than 24 hours
176- cleaned = await exec_repo .cleanup_old (hours = 24 )
154+ # Clean up execs older than configured retention period
155+ cleaned = await exec_repo .cleanup_old (hours = EXEC_RETENTION_HOURS )
177156
178157 logger .info ("Cleaned up old execs" , extra = {"count" : cleaned })
179158 return cleaned
@@ -192,14 +171,11 @@ async def _cleanup_abandoned_attachments(self) -> int:
192171 logger .info ("Cleaning up abandoned attachments" )
193172
194173 try :
195- async with self .db_manager .get_session () as session :
196- attachment_repo = AttachmentRepository (session )
197-
198- # Get all attachments
199- # In a full implementation, we would identify abandoned ones
200- # For now, just log
201- logger .info ("Attachment cleanup completed" )
202- return 0
174+ # Get all attachments
175+ # In a full implementation, we would identify abandoned ones
176+ # For now, just log
177+ logger .info ("Attachment cleanup completed" )
178+ return 0
203179
204180 except Exception as e :
205181 logger .error ("Failed to clean up attachments" , extra = {"error" : str (e )})
@@ -250,7 +226,7 @@ async def _sync_container_state(self) -> int:
250226
251227 synced += 1
252228
253- except Exception :
229+ except NotFound :
254230 # Container doesn't exist, mark as stopped
255231 if container .status != "stopped" :
256232 await repo .update_status (container .id , "stopped" )
@@ -272,11 +248,12 @@ async def _vacuum_database(self) -> None:
272248 logger .info ("Vacuuming database" )
273249
274250 try :
275- from sqlalchemy import text
276-
277251 async with self .db_manager .get_session () as session :
278- # Execute VACUUM command
252+ from sqlalchemy import text
253+
254+ # Execute VACUUM command using raw SQL
279255 await session .execute (text ("VACUUM" ))
256+ await session .commit ()
280257 logger .info ("Database vacuumed successfully" )
281258
282259 except Exception as e :
@@ -307,7 +284,6 @@ async def check_health(self) -> dict:
307284 health ["containers_count" ] = len (containers )
308285
309286 # Count active execs
310- exec_repo = ExecRepository (session )
311287 # In a full implementation, would count incomplete execs
312288 health ["active_execs" ] = 0
313289
0 commit comments