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