How the Music Assistant Task Manager Handles Background Tasks
The Music Assistant server uses a dedicated TasksController in music_assistant/controllers/tasks/controller.py to schedule, queue, and monitor asynchronous background tasks with built-in concurrency limits, persistence, and progress tracking.
The Music Assistant task manager background tasks system provides the infrastructure for long-running operations like library scans and metadata refreshes without blocking the main event loop. Located in the music_assistant.controllers.tasks package within the music-assistant/server repository, this subsystem manages everything from ad-hoc job queuing to recurring scheduled maintenance.
Core Architecture – The TasksController
The TasksController class in music_assistant/controllers/tasks/controller.py serves as the central nervous system for all background operations. It inherits from CoreController, giving it access to the main MusicAssistant instance and standard lifecycle hooks.
Task Registration and Storage
The controller distinguishes between two primary task types:
- Ad-hoc tasks – Created via
run_background_task()for one-time operations like user-initiated playlist refreshes - Recurring tasks – Registered via
register_scheduled_task()for periodic maintenance like daily library cleanup
Both methods create a BackgroundTask model (from music_assistant_models.background_task) and wrap it in a ManagedTask container defined in models.py. The controller stores these in self._tasks, a dictionary mapping task IDs to ManagedTask instances.
Queueing and Execution Logic
Tasks enter a deque called self._pending_task_ids where priority tasks (flagged with priority=True) are inserted at the front. The controller respects self._max_concurrent_tasks, defaulting to 2 concurrent executions as defined in constants.py.
When the worker loop picks a task ID, it creates an asyncio.Task to run the provider-supplied coroutine. During execution, the ACTIVE_TASK_ID context variable (defined in constants.py) is set so that nested calls can locate the current task via TaskExecutionContext from context.py.
Scheduling and Persistence
Recurring tasks integrate with the server's timer subsystem using self.mass.schedule_timer and self.mass.cancel_timer. The next-run timestamp is stored in BackgroundTask.schedule.next_run and persisted under the config key TASK_STATE_CONFIG_KEY ("scheduled_task_states").
On shutdown, the controller unregisters all tasks, removes the TaskLogHandler, and clears persisted schedule state to ensure clean restarts.
Supporting Infrastructure
Constants and Configuration
The constants.py file defines critical operational limits:
DEFAULT_TASK_LOG_LINES(250) – Maximum log entries retained per taskMAX_FINISHED_TASK_HISTORY(100) – Cap on completed non-recurring tasksTASK_LIFECYCLE_UPDATE_DEBOUNCE(0.25 seconds) – Debounce period for UI updates
Execution Context and Logging
The helpers.py module provides TaskLogHandler, a custom logging handler that intercepts output from any Python logging call made within a task. Log lines are trimmed to the 250-line limit and stored in the task's logs list. The update_task_progress method allows providers to report completion percentages and status messages.
State Serialization
Helper functions serialize_task_state and restore_task_state in helpers.py handle JSON encoding of schedule states, enabling task persistence across server restarts.
Implementing Background Tasks
Providers and controllers interact with the task manager through straightforward async APIs.
Registering a Recurring Task
# From a provider or core controller
tasks_controller.register_scheduled_task(
task_id="library_cleanup",
name="Library cleanup",
handler=cleanup_library, # async callable
schedule=TaskSchedule(interval=86_400), # every 24 hours
translation_key="background_task.library_cleanup",
)
Running an Ad-Hoc Task
# Queue immediate execution with priority
await tasks_controller.run_background_task(
name="Refresh playlist",
handler=refresh_playlist, # async callable
translation_key="background_task.refresh_playlist",
user_id=current_user.user_id,
priority=True, # insert at front of queue
)
Querying via WebSocket API
Clients can retrieve visible tasks through the WebSocket API:
await websocket.send_json({"command": "tasks/list"})
response = await websocket.receive_json()
for task in response["result"]:
print(f"{task['name']} – {task['status']} (progress {task.get('progress', 0)}%)")
API Integration and Security
The TasksController exposes several @api_command decorated methods including tasks/list, tasks/get, tasks/run, and tasks/cancel. The permission model uses UserRole enforcement—only administrators can execute, retry, or cancel arbitrary tasks.
Non-admin users are restricted to viewing tasks they started via list_tasks_for_user and _get_visible_managed_task, which validate task_info.user_id against the requesting user.
Summary
- The
TasksControllerinmusic_assistant/controllers/tasks/controller.pymanages the full lifecycle of background tasks with a default concurrency limit of 2. - Task persistence utilizes the config key
"scheduled_task_states"to survive server restarts. - Thread safety is enforced by checking
get_ident()againstself.mass.loop_thread_idand rescheduling viacall_soon_threadsafe. - Progress tracking combines
update_task_progresscalls with automatic log capture viaTaskLogHandler. - API security restricts sensitive operations to admin users while allowing non-admins to view their own tasks.
Frequently Asked Questions
How does Music Assistant limit concurrent background tasks?
The TasksController enforces a concurrency cap of 2 by default through self._max_concurrent_tasks defined in constants.py. The worker loop only creates new asyncio.Task instances when the number of active tasks falls below this limit, queueing additional task IDs in self._pending_task_ids until capacity becomes available.
Where are scheduled task states stored in Music Assistant?
Scheduled task states are serialized using serialize_task_schedule_state from helpers.py and stored under the configuration key "scheduled_task_states" (TASK_STATE_CONFIG_KEY). This JSON-encoded state persists the next_run timestamps and schedule parameters, allowing recurring tasks to resume their cadence after server restarts.
How can providers update task progress?
Providers call self.tasks.update_task_progress(progress, text) where progress is an integer percentage and text is a descriptive message. These updates are automatically debounced by 0.25 seconds (TASK_LIFECYCLE_UPDATE_DEBOUNCE) to prevent UI flooding. Additionally, any standard Python logging output from the task is captured by TaskLogHandler and stored in the task's logs list (capped at 250 lines).
What happens to finished tasks in the Music Assistant task manager?
Completed non-recurring tasks are retained in a history buffer capped at 100 entries (MAX_FINISHED_TASK_HISTORY). Administrators can manually clear this history via the tasks/clear_finished API command. On controller shutdown, all tasks are unregistered and log handlers removed to ensure a clean state for the next server launch.
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 →