Run garbage collection to clean up orphaned containers and old volumes. This is best-effort and non-critical - don't let GC errors block the build. Runs in a subprocess with timeout to prevent hanging.
()
| 213 | |
| 214 | |
| 215 | def _run_garbage_collection() -> None: |
| 216 | """Run garbage collection to clean up orphaned containers and old volumes. |
| 217 | |
| 218 | This is best-effort and non-critical - don't let GC errors block the build. |
| 219 | Runs in a subprocess with timeout to prevent hanging. |
| 220 | """ |
| 221 | import concurrent.futures |
| 222 | |
| 223 | def _gc_task() -> tuple[int, int]: |
| 224 | """Run GC in a separate thread with timeout protection.""" |
| 225 | db = ContainerDatabase() |
| 226 | orphans = cleanup_orphaned_containers(db) |
| 227 | old = garbage_collect_platform_containers( |
| 228 | "unit-tests", max_containers=MAX_CONTAINERS_PER_PROJECT, db=db |
| 229 | ) |
| 230 | return orphans, old |
| 231 | |
| 232 | try: |
| 233 | # Run GC with a 30-second timeout to prevent hanging on Docker API errors |
| 234 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: |
| 235 | future = executor.submit(_gc_task) |
| 236 | try: |
| 237 | orphans_cleaned, old_cleaned = future.result(timeout=30) |
| 238 | if orphans_cleaned > 0: |
| 239 | ts_print(f"Cleaned up {orphans_cleaned} orphaned container(s)") |
| 240 | if old_cleaned > 0: |
| 241 | ts_print( |
| 242 | f"Garbage collected {old_cleaned} old unit-test container(s)" |
| 243 | ) |
| 244 | except concurrent.futures.TimeoutError: |
| 245 | ts_print("Warning: Garbage collection timed out (30s) - skipping") |
| 246 | future.cancel() |
| 247 | except KeyboardInterrupt as ki: |
| 248 | handle_keyboard_interrupt(ki) |
| 249 | raise |
| 250 | except Exception as e: |
| 251 | # Don't fail the build if garbage collection fails |
| 252 | ts_print(f"Warning: Garbage collection failed: {e}") |
| 253 | |
| 254 | |
| 255 | def _build_sync_script() -> str: |
no test coverage detected