How TaskManager and SessionManager Manage Download Tasks and State Persistence in Motrix
Motrix separates in-memory task coordination from durable state persistence by delegating real-time download tracking to TaskManager and periodic snapshotting to SessionManager, ensuring atomic updates and crash recovery through a reservation system that maps aria2 engine IDs to Motrix task IDs.
Motrix, the open-source download manager built on aria2, implements a robust two-layer architecture to handle download lifecycle management and data durability. The separation between TaskManager and SessionManager ensures that transient engine states never corrupt persisted data while maintaining real-time synchronization with the aria2 backend. Understanding how these components coordinate is essential for developers extending Motrix or troubleshooting task recovery issues.
Architectural Separation of Concerns
TaskManager: The In-Memory Authority
Located in src/core/task/task-manager.ts, the TaskManager serves as the single source of truth for all active downloads during runtime. It maintains a primary map keyed by Motrix-specific task IDs, storing DownloadTask objects that encapsulate metadata, progress, and status.
The manager implements several critical indexing mechanisms to handle the asynchronous nature of the aria2 engine:
- Engine Index: A secondary mapping that translates aria2 engine task IDs (gids) to Motrix task IDs, enabling reverse lookups when the engine reports status updates.
- Reservation System: Before aria2 creates a task row,
reserveEngineTaskId(gid)claims the future engine ID to prevent orphan detection during polling intervals. - Retired Shield: The
retiredEngineGidsset protects against transient engine rows that persist briefly after task removal or ID swaps, preventing duplicate entries.
Cleanup operations like remove() and clear() handle de-indexing logic, ensuring the engine index and primary task map remain consistent when tasks complete or get deleted.
SessionManager: Durable State Persistence
The SessionManager in src/core/session/session-manager.ts owns the durability layer, bridging the volatile TaskManager with the SQLite backend. It orchestrates periodic snapshots of the entire task collection to the task_instances table while managing the RPC client connection to aria2.
Key responsibilities include:
- Atomic Persistence: The
runExclusivePersistence()method acquires a write-lock on the database, ensuring thatsave()operations (default 15-second intervals) never interleave with concurrent mutations. - State Restoration: On application startup,
restore()reconstructsDownloadTaskobjects from persisted rows and re-injects them into TaskManager viataskManager.set(), rebuilding the engine index from stored gids. - Immediate Flush: APIs like
persistTask(),persistTaskWithOccurrence(), andsaveNow()allow UI components to force synchronous writes when critical state changes occur.
Coordination Flow Between Managers
The interaction between TaskManager and SessionManager follows a strict protocol to maintain consistency:
-
Task Creation: UI components create a
DownloadTaskinstance and register it viataskManager.add(task), making it available for immediate engine operations. -
Engine ID Reservation: Before submitting the download request to aria2, the system calls
taskManager.reserveEngineTaskId(gid)to claim the engine task ID. This reservation prevents SessionManager's polling logic from treating the upcoming aria2 row as an orphaned entry. -
GID Binding: When aria2 confirms the download with a concrete gid,
taskManager.setReservedEngineTaskOwner(id, task, gid)swaps the reservation for the actual task binding, activating the engine index entry. -
Periodic Checkpointing: Every 15 seconds (configurable),
sessionManager.save()iterates overtaskManager.getAll(), serializing each task to thetask_instancestable through the atomic persistence wrapper. -
Graceful Recovery: During application startup,
sessionManager.restore()hydrates the TaskManager from disk, re-establishing the engine index and merging live aria2 data with the recovered state.
Implementation Examples
The following TypeScript examples demonstrate typical usage patterns in Motrix's main process:
// Initialize core managers
import { TaskManager } from '@core/task/task-manager';
import { SessionManager } from '@core/session/session-manager';
import { createRpcClient } from '@core/rpc/client';
import { createDbAdapter } from '@core/db/adapter';
const taskManager = new TaskManager();
const sessionManager = new SessionManager(
taskManager,
createRpcClient(),
createDbAdapter()
);
// Start automatic persistence (15s default interval)
sessionManager.startAutoSave();
Adding a new download and triggering immediate persistence:
import { createDownloadTask } from '@shared/helpers/task';
const task = createDownloadTask({
url: 'https://example.com/file.zip',
fileName: 'file.zip'
});
taskManager.add(task);
sessionManager.saveNow(); // Force immediate flush to database
Handling engine ID reservations before aria2 submission:
const futureGid = '1234567890abcdef';
taskManager.reserveEngineTaskId(futureGid);
// After aria2 confirmation:
taskManager.setReservedEngineTaskOwner(task.id, task, futureGid);
Restoring state after application restart:
await sessionManager.restore();
// TaskManager now contains all previously active downloads
Key Source Files
| File | Responsibility |
|---|---|
src/core/task/task-manager.ts |
In-memory task collection, engine-to-task ID mapping, reservation and retired-gid shields |
src/core/session/session-manager.ts |
Periodic persistence, atomic database writes, startup restoration, RPC coordination |
src/server/task-persistence.ts |
Thin wrapper delegating persistence operations to SessionManager |
src/main/index.ts |
Entry point instantiating both managers and launching the autosave loop |
Summary
- TaskManager maintains the runtime authority over download metadata, using bidirectional indexing (Motrix ID to task, gid to Motrix ID) to reconcile engine state with application state.
- SessionManager guarantees durability through atomic database transactions, persisting the complete task collection every 15 seconds and reconstructing it on startup.
- The reservation system bridges the gap between task creation and engine acknowledgment, preventing race conditions during aria2 polling.
- Retired shields protect against transient engine artifacts that could otherwise spawn duplicate task entries.
- Together, these components ensure that Motrix survives crashes and restarts without losing download progress or creating inconsistent state between the UI and aria2 backend.
Frequently Asked Questions
What happens if Motrix crashes between 15-second save intervals?
The SessionManager persists tasks atomically every 15 seconds by default. If a crash occurs between intervals, only the state changes since the last checkpoint are lost. When Motrix restarts, SessionManager.restore() loads the last complete snapshot from the task_instances table and merges it with live aria2 data, reconstructing the task queue to its last persisted state.
How does TaskManager prevent duplicate tasks when aria2 reports the same gid twice?
TaskManager implements a reservation system via reserveEngineTaskId() and setReservedEngineTaskOwner(). Before submitting to aria2, the gid is reserved; when the engine confirms, the reservation converts to an active mapping in the engine index. Additionally, the retiredEngineGids set shields against recently removed gids that might appear in final polling cycles, preventing orphan rows from re-entering the task collection.
Can I adjust the autosave interval in Motrix?
Yes. The SessionManager accepts configuration for the autosave interval when instantiated, though the default is 15 seconds. You can force immediate persistence at any time by calling sessionManager.saveNow(), which is useful when handling critical UI actions like pause/resume or configuration changes that must survive an unexpected termination.
Why does SessionManager use exclusive locking for database writes?
The runExclusivePersistence() method ensures that database writes occur atomically relative to in-memory mutations. Without this lock, a task update could modify the TaskManager state during a save() operation, resulting in a corrupted snapshot where some tasks reflect old states and others reflect new states. The write-lock guarantees consistency between the in-memory TaskManager and the persisted task_instances rows.
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 →