|
| 1 | +import asyncio |
1 | 2 | import uuid |
| 3 | +from datetime import datetime, timedelta, timezone |
2 | 4 | from typing import Any |
3 | 5 | from uuid import UUID |
4 | 6 |
|
| 7 | +from prefect import State |
5 | 8 | from prefect.client.orchestration import PrefectClient, get_client |
6 | 9 | from prefect.client.schemas.filters import ( |
7 | 10 | ArtifactFilter, |
|
12 | 15 | FlowRunFilter, |
13 | 16 | FlowRunFilterId, |
14 | 17 | FlowRunFilterName, |
| 18 | + FlowRunFilterStartTime, |
15 | 19 | FlowRunFilterState, |
16 | 20 | FlowRunFilterStateType, |
17 | 21 | FlowRunFilterTags, |
@@ -311,3 +315,72 @@ async def query( |
311 | 315 | ) |
312 | 316 |
|
313 | 317 | return {"count": count or 0, "edges": nodes} |
| 318 | + |
| 319 | + @classmethod |
| 320 | + async def delete_flow_runs( |
| 321 | + cls, |
| 322 | + states: list[StateType] = [StateType.COMPLETED, StateType.FAILED, StateType.CANCELLED], # noqa: B006 |
| 323 | + delete: bool = True, |
| 324 | + days_to_keep: int = 2, |
| 325 | + batch_size: int = 100, |
| 326 | + ) -> None: |
| 327 | + """Delete flow runs in the specified states and older than specified days.""" |
| 328 | + |
| 329 | + logger = get_logger() |
| 330 | + |
| 331 | + async with get_client(sync_client=False) as client: |
| 332 | + cutoff = datetime.now(timezone.utc) - timedelta(days=days_to_keep) |
| 333 | + |
| 334 | + flow_run_filter = FlowRunFilter( |
| 335 | + start_time=FlowRunFilterStartTime(before_=cutoff), # type: ignore[arg-type] |
| 336 | + state=FlowRunFilterState(type=FlowRunFilterStateType(any_=states)), |
| 337 | + ) |
| 338 | + |
| 339 | + # Get flow runs to delete |
| 340 | + flow_runs = await client.read_flow_runs(flow_run_filter=flow_run_filter, limit=batch_size) |
| 341 | + |
| 342 | + deleted_total = 0 |
| 343 | + |
| 344 | + while True: |
| 345 | + batch_deleted = 0 |
| 346 | + failed_deletes = [] |
| 347 | + |
| 348 | + # Delete each flow run through the API |
| 349 | + for flow_run in flow_runs: |
| 350 | + try: |
| 351 | + if delete: |
| 352 | + await client.delete_flow_run(flow_run_id=flow_run.id) |
| 353 | + else: |
| 354 | + await client.set_flow_run_state( |
| 355 | + flow_run_id=flow_run.id, |
| 356 | + state=State(type=StateType.CRASHED), |
| 357 | + force=True, |
| 358 | + ) |
| 359 | + deleted_total += 1 |
| 360 | + batch_deleted += 1 |
| 361 | + except Exception as e: |
| 362 | + logger.warning(f"Failed to delete flow run {flow_run.id}: {e}") |
| 363 | + failed_deletes.append(flow_run.id) |
| 364 | + |
| 365 | + # Rate limiting |
| 366 | + if batch_deleted % 10 == 0: |
| 367 | + await asyncio.sleep(0.5) |
| 368 | + |
| 369 | + logger.info(f"Delete {batch_deleted}/{len(flow_runs)} flow runs (total: {deleted_total})") |
| 370 | + |
| 371 | + # Get next batch |
| 372 | + previous_flow_run_ids = [fr.id for fr in flow_runs] |
| 373 | + flow_runs = await client.read_flow_runs(flow_run_filter=flow_run_filter, limit=batch_size) |
| 374 | + |
| 375 | + if not flow_runs: |
| 376 | + logger.info("No more flow runs to delete") |
| 377 | + break |
| 378 | + |
| 379 | + if previous_flow_run_ids == [fr.id for fr in flow_runs]: |
| 380 | + logger.info("Found same flow runs to delete, aborting") |
| 381 | + break |
| 382 | + |
| 383 | + # Delay between batches to avoid overwhelming the API |
| 384 | + await asyncio.sleep(1.0) |
| 385 | + |
| 386 | + logger.info(f"Retention complete. Total deleted tasks: {deleted_total}") |
0 commit comments