Skip to content

Commit 1cc2357

Browse files
Copilotpvliesdonk
andcommitted
Merge main into Epic 7 branch - resolve conflicts and fix linting
Co-authored-by: pvliesdonk <22190282+pvliesdonk@users.noreply.github.com>
1 parent 185031a commit 1cc2357

File tree

8 files changed

+122
-149
lines changed

8 files changed

+122
-149
lines changed

src/mcp_devbench/managers/maintenance_manager.py

Lines changed: 27 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
"""Background maintenance manager for periodic tasks."""
22

33
import asyncio
4-
from datetime import datetime, timedelta, timezone
54

65
from docker import DockerClient
6+
from docker.errors import NotFound
77

88
from mcp_devbench.config import get_settings
99
from mcp_devbench.models.database import get_db_manager
10-
from mcp_devbench.repositories.attachments import AttachmentRepository
1110
from mcp_devbench.repositories.containers import ContainerRepository
1211
from mcp_devbench.repositories.execs import ExecRepository
1312
from mcp_devbench.utils import get_logger
13+
from mcp_devbench.utils.cleanup import cleanup_orphaned_transients
1414
from mcp_devbench.utils.docker_client import get_docker_client
1515

1616
logger = 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

1926
class 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

src/mcp_devbench/managers/reconciliation_manager.py

Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
"""Reconciliation manager for boot recovery and state synchronization."""
22

3-
from datetime import datetime, timedelta, timezone
3+
from datetime import datetime, timezone
44
from typing import List
55

66
from docker import DockerClient
7-
from docker.errors import APIError, NotFound
7+
from docker.errors import APIError
88
from docker.models.containers import Container as DockerContainer
99

1010
from mcp_devbench.config import get_settings
1111
from mcp_devbench.models.containers import Container
1212
from mcp_devbench.models.database import get_db_manager
1313
from mcp_devbench.repositories.containers import ContainerRepository
14-
from mcp_devbench.repositories.execs import ExecRepository
1514
from mcp_devbench.utils import get_logger
15+
from mcp_devbench.utils.cleanup import cleanup_orphaned_transients
1616
from mcp_devbench.utils.docker_client import get_docker_client
1717

1818
logger = get_logger(__name__)
@@ -228,50 +228,10 @@ async def _handle_orphaned_transients(self, session) -> int:
228228
Returns:
229229
Number of containers cleaned up
230230
"""
231-
cutoff_days = self.settings.transient_gc_days
232-
cutoff = datetime.now(timezone.utc) - timedelta(days=cutoff_days)
233-
234231
repo = ContainerRepository(session)
235-
transients = await repo.list_by_status("stopped", persistent=False)
236-
237-
cleaned = 0
238-
for container in transients:
239-
# Check if container is old enough
240-
if container.last_seen < cutoff:
241-
try:
242-
# Try to remove Docker container if it exists
243-
try:
244-
docker_container = self.docker_client.containers.get(
245-
container.docker_id
246-
)
247-
docker_container.remove(force=True)
248-
logger.info(
249-
"Removed orphaned Docker container",
250-
extra={
251-
"container_id": container.id,
252-
"docker_id": container.docker_id,
253-
},
254-
)
255-
except NotFound:
256-
pass
257-
258-
# Remove from database
259-
await repo.delete(container.id)
260-
cleaned += 1
261-
262-
logger.info(
263-
"Cleaned up orphaned transient container",
264-
extra={
265-
"container_id": container.id,
266-
"age_days": (datetime.now(timezone.utc) - container.last_seen).days,
267-
},
268-
)
269-
except Exception as e:
270-
logger.error(
271-
"Failed to clean up orphaned container",
272-
extra={"container_id": container.id, "error": str(e)},
273-
)
274-
232+
cleaned = await cleanup_orphaned_transients(
233+
self.docker_client, repo, self.settings.transient_gc_days
234+
)
275235
return cleaned
276236

277237
async def _cleanup_incomplete_execs(self, session) -> None:
@@ -281,14 +241,19 @@ async def _cleanup_incomplete_execs(self, session) -> None:
281241
Args:
282242
session: Database session
283243
"""
284-
exec_repo = ExecRepository(session)
285-
286244
# Get all incomplete execs (no end time)
287245
# For simplicity, we'll just log this for now
288246
# A full implementation would query for incomplete execs and mark them
289247
logger.info("Incomplete exec cleanup completed")
290248

291249

250+
# Global instance
251+
_reconciliation_manager: ReconciliationManager | None = None
252+
253+
292254
def get_reconciliation_manager() -> ReconciliationManager:
293-
"""Get reconciliation manager instance."""
294-
return ReconciliationManager()
255+
"""Get or create reconciliation manager instance."""
256+
global _reconciliation_manager
257+
if _reconciliation_manager is None:
258+
_reconciliation_manager = ReconciliationManager()
259+
return _reconciliation_manager

src/mcp_devbench/managers/shutdown_coordinator.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Shutdown coordinator for graceful server shutdown."""
22

33
import asyncio
4-
import signal
5-
from typing import Callable
64

75
from mcp_devbench.config import get_settings
86
from mcp_devbench.managers.container_manager import ContainerManager
@@ -160,24 +158,3 @@ def get_shutdown_coordinator() -> ShutdownCoordinator:
160158
if _shutdown_coordinator is None:
161159
_shutdown_coordinator = ShutdownCoordinator()
162160
return _shutdown_coordinator
163-
164-
165-
def setup_signal_handlers(shutdown_handler: Callable[[], None]) -> None:
166-
"""
167-
Set up signal handlers for graceful shutdown.
168-
169-
Args:
170-
shutdown_handler: Function to call on SIGTERM/SIGINT
171-
"""
172-
173-
def signal_handler(signum, frame):
174-
"""Handle shutdown signals."""
175-
sig_name = signal.Signals(signum).name
176-
logger.info(f"Received {sig_name} signal, initiating shutdown")
177-
shutdown_handler()
178-
179-
# Register handlers for SIGTERM and SIGINT
180-
signal.signal(signal.SIGTERM, signal_handler)
181-
signal.signal(signal.SIGINT, signal_handler)
182-
183-
logger.info("Signal handlers registered for graceful shutdown")

src/mcp_devbench/repositories/containers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ async def get_by_identifier(self, identifier: str) -> Container | None:
7070
return await self.get_by_alias(identifier)
7171

7272
async def list_by_status(
73-
self, status: str | None = None, include_stopped: bool = False, persistent: bool | None = None
73+
self,
74+
status: str | None = None,
75+
include_stopped: bool = False,
76+
persistent: bool | None = None,
7477
) -> List[Container]:
7578
"""
7679
List containers by status.

src/mcp_devbench/repositories/execs.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,10 @@ async def cleanup_old(self, hours: int = 24) -> int:
104104
"""
105105
from datetime import timedelta, timezone
106106

107+
from sqlalchemy import delete
108+
107109
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
108-
stmt = select(Exec).where(Exec.ended_at.is_not(None), Exec.ended_at < cutoff)
110+
stmt = delete(Exec).where(Exec.ended_at.is_not(None), Exec.ended_at < cutoff)
109111
result = await self.session.execute(stmt)
110-
old_execs = list(result.scalars().all())
111-
112-
for exec_entry in old_execs:
113-
await self.delete(exec_entry.id)
114112

115-
return len(old_execs)
113+
return result.rowcount

src/mcp_devbench/server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
from mcp_devbench.models.database import close_db, get_db_manager, init_db
5151
from mcp_devbench.repositories.attachments import AttachmentRepository
5252
from mcp_devbench.repositories.containers import ContainerRepository
53-
from mcp_devbench.repositories.execs import ExecRepository
5453
from mcp_devbench.utils import get_logger, setup_logging
5554
from mcp_devbench.utils.audit_logger import AuditEventType, get_audit_logger
5655
from mcp_devbench.utils.docker_client import close_docker_client, get_docker_client

0 commit comments

Comments
 (0)