How Pixelle-Video Handles Task Cleanup: A Deep Dive into the Async TaskManager
Pixelle-Video automatically cleans up finished tasks through a periodic _cleanup_loop coroutine that removes completed, failed, or cancelled tasks after a configurable retention window, ensuring no resource leaks when the server shuts down.
The open-source Pixelle-Video project (AIDC-AI/Pixelle-Video) provides a robust background task system for AI video generation. Understanding how it handles task cleanup is critical for production deployments where memory management and resource constraints matter. This article examines the complete cleanup architecture from configuration to shutdown.
Core Cleanup Architecture
Pixelle-Video's cleanup system centers on three interlocking components defined in api/tasks/manager.py:
| Component | Purpose | Key Method |
|---|---|---|
| Task lifecycle management | Create, track, and execute async tasks | create_task(), execute_task() |
| Periodic cleanup loop | Continuously purge stale tasks | _cleanup_loop() |
| Old task removal | Apply retention policy to finished tasks | _cleanup_old_tasks() |
The TaskManager class orchestrates these operations through a singleton pattern exposed as task_manager throughout the application.
Starting and Stopping the Cleanup Loop
The cleanup lifecycle begins when the FastAPI server starts. In api/app.py, the lifespan context manager initializes the system:
# api/app.py (lines 68-77)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Initialize the task manager
await task_manager.start()
yield
# Shutdown: cancel all pending tasks and cleanup
await task_manager.stop()
The start() method in api/tasks/manager.py (lines 46-71) creates the persistent cleanup task:
# api/tasks/manager.py – start() implementation
def start(self):
"""Start the background cleanup loop."""
self._cleanup_task = asyncio.create_task(
self._cleanup_loop(),
name="task_cleanup_loop"
)
When the server shuts down, stop() (lines 66-71) ensures graceful termination:
- Cancels the
_cleanup_looptask - Cancels all pending
Futureobjects viafuture.cancel() - Clears the internal
_tasksand_task_futuresdictionaries
This guarantees no dangling references or leaked asyncio tasks remain.
The Cleanup Loop: _cleanup_loop
The heart of the system is _cleanup_loop (lines 232-242), an infinite coroutine that drives periodic cleanup:
# api/tasks/manager.py – _cleanup_loop (lines 232-242)
async def _cleanup_loop(self):
"""Background task that periodically cleans up old tasks."""
while True:
try:
await asyncio.sleep(self._cleanup_interval)
await self._cleanup_old_tasks()
except asyncio.CancelledError:
# Expected on shutdown
break
except Exception as e:
logger.error(f"Error in cleanup loop: {e}")
Key characteristics:
- Configurable interval: controlled by
task_cleanup_interval(default 3600 seconds / 1 hour) - Exception resilience: logs errors but continues running; only
CancelledErrorbreaks the loop - Non-blocking: uses
asyncio.sleepto yield control between runs
Applying Retention Policy: _cleanup_old_tasks
The actual removal logic resides in _cleanup_old_tasks (lines 445-461):
# api/tasks/manager.py – _cleanup_old_tasks (lines 445-461)
async def _cleanup_old_tasks(self):
"""Remove tasks that have exceeded the retention time."""
cutoff = datetime.utcnow() - timedelta(seconds=self._retention_time)
expired_tasks = [
task_id for task_id, task in self._tasks.items()
if task.status in (TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED)
and task.completed_at and task.completed_at < cutoff
]
for task_id in expired_tasks:
del self._tasks[task_id]
if task_id in self._task_futures:
self._task_futures[task_id].cancel()
del self._task_futures[task_id]
if expired_tasks:
logger.info(f"Cleaned up {len(expired_tasks)} expired tasks")
This method implements a three-state cleanup eligibility:
| Status | Eligible for Cleanup? |
|---|---|
COMPLETED |
✅ Yes |
FAILED |
✅ Yes |
CANCELLED |
✅ Yes |
PENDING or RUNNING |
❌ No |
The retention calculation uses completed_at timestamp compared against task_retention_time (default 86400 seconds / 24 hours).
Configuration Settings
Cleanup behavior is controlled in api/config.py (lines 33-37):
# api/config.py – cleanup configuration (lines 33-37)
class APIConfig(BaseSettings):
# How often to run the cleanup loop (seconds)
task_cleanup_interval: int = 3600 # 1 hour
# How long to keep finished task results (seconds)
task_retention_time: int = 86400 # 24 hours
These environment-configurable values allow deployment-specific tuning:
- High-volume services: Reduce
task_cleanup_intervalto minutes - Compliance requirements: Adjust
task_retention_timefor audit trails - Memory-constrained environments: Lower retention to free RAM faster
Complete Cleanup Flow Example
Here's how task cleanup operates from creation to removal:
from api.tasks import task_manager, TaskType
import asyncio
async def demonstrate_cleanup():
# 1. Create a video generation task
task = task_manager.create_task(
task_type=TaskType.VIDEO_GENERATION,
request_params={"prompt": "A robot dancing in the rain"}
)
print(f"Created task: {task.task_id}")
# 2. Execute the work (simulated here)
await task_manager.execute_task(
task_id=task.task_id,
coro_func=asyncio.sleep, # stand-in for actual video generation
delay=0.1
)
# 3. Task is now COMPLETED with completed_at timestamp
# 4. Cleanup loop (running every hour) will eventually call:
# _cleanup_old_tasks() → checks if completed_at < now - 24h
# → deletes task if expired
# 5. On server shutdown, lifespan calls task_manager.stop():
# → cancels cleanup loop
# → cancels all futures
# → clears all dictionaries
# Run with: asyncio.run(demonstrate_cleanup())
Summary
- Start/stop lifecycle:
task_manager.start()launches_cleanup_loop;task_manager.stop()cancels it and clears all state - Periodic execution:
_cleanup_loopsleeps fortask_cleanup_interval(default 1 hour) between cleanup runs - Retention policy:
_cleanup_old_tasksremoves tasks withCOMPLETED,FAILED, orCANCELLEDstatus whencompleted_atexceedstask_retention_time(default 24 hours) - Graceful shutdown: All pending futures are cancelled and dictionaries cleared to prevent memory leaks
- Configuration: Cleanup timing is controlled via
APIConfiginapi/config.py
Frequently Asked Questions
How often does Pixelle-Video clean up finished tasks?
By default, the cleanup loop runs every 3600 seconds (1 hour). This is controlled by the task_cleanup_interval setting in api/config.py. You can reduce this to minutes for high-traffic deployments or increase it for lower resource usage.
What determines when a task becomes eligible for cleanup?
A task must meet two conditions: its status must be COMPLETED, FAILED, or CANCELLED (not PENDING or RUNNING), and its completed_at timestamp must be older than the retention window. The default task_retention_time is 86400 seconds (24 hours).
What happens to running tasks when the server shuts down?
When the FastAPI server shuts down, the lifespan context manager in api/app.py calls task_manager.stop(). This cancels the cleanup loop, cancels all pending Future objects via future.cancel(), and clears the internal _tasks and _task_futures dictionaries to ensure no resource leaks.
Can I customize the retention policy for different task types?
The current implementation in api/tasks/manager.py uses global task_cleanup_interval and task_retention_time values from APIConfig. There is no per-task-type retention in the base implementation. To implement custom retention, you would need to modify _cleanup_old_tasks to check task.task_type against a type-specific retention mapping.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →