Skip to content
Documentation GitHub
Platform

Task Runner System

Status: Implemented Depends On: Tokio runtime Crate: crates/infrastructure/task-runner/


The Task Runner System provides unified background task execution for the Inklings desktop application. It supports two task types — event-driven tasks that react to application events via bounded channels, and scheduled tasks that run on fixed intervals. A single TaskRunner instance is created per workspace and manages all background work for that workspace’s lifetime.

The system provides a shared execution framework for any subsystem that needs background processing without blocking the UI thread.


TaskRunner (per workspace)
├── Event-driven tasks
│ └── EmbeddingTask apps/desktop/src-tauri/src/embedding.rs
│ Reacts to page saves, indexes workspace content
└── Scheduled tasks
└── HistoryCollapseTask apps/desktop/src-tauri/src/history_collapse.rs
Prunes event_log entries beyond retention window (24h interval)
  1. Workspace open: A new TaskRunner is created in open_workspace_async(). Tasks are registered before start() is called.
  2. Running: The runner spawns a supervisor tokio task that owns all child task join handles. Each task runs in its own tokio task with structured tracing spans.
  3. Workspace close: The runner is taken from AppState.managers.task_runner and shutdown_and_wait() is called with a bounded timeout (5 seconds). The CancellationToken signals all tasks to stop.
  4. App exit: The same shutdown sequence runs in the Tauri on_event(CloseRequested) handler. If the runner is dropped without explicit shutdown, the Drop impl cancels all tasks (fire-and-forget).
AppState
└── managers: ManagerBundle
└── task_runner: Mutex<Option<TaskRunner>>
Some(_) when workspace is open
None when no workspace is active

The get_task_runner_health Tauri command exposes per-task health snapshots to the frontend for diagnostics.


Event-driven tasks implement the EventDrivenTask trait:

pub trait EventDrivenTask: Send + Sync + 'static {
type Event: Send + 'static + Eq + Hash;
fn name(&self) -> &'static str;
fn channel_capacity(&self) -> usize; // default: 256
fn debounce_interval(&self) -> Option<Duration>; // default: None
fn handle_batch(&self, events: Vec<Self::Event>)
-> impl Future<Output = Result<(), TaskError>> + Send;
}

Key behaviors:

  • Bounded channel with backpressure: Each task gets a mpsc::channel with configurable capacity. EventTaskHandle::try_send() returns TaskSendError::Full when the channel is at capacity — events are dropped rather than blocking the caller.
  • Batch processing: Events are drained from the channel and delivered as a Vec to handle_batch(). This amortizes overhead for bursty workloads.
  • Debounce with deduplication: When debounce_interval() returns Some(duration), events accumulate in a HashSet (deduplicating by Eq + Hash) and are flushed only after the debounce timer expires. The timer resets on each new event. The Sleep future is allocated once and reset in-place via Sleep::reset() to avoid per-event allocation.
  • Lock-free queue depth: An AtomicUsize counter tracks queue depth on the hot try_send path without acquiring the health mutex.
  • Status broadcasting: A watch::channel provides TaskStatus updates that consumers can subscribe to (e.g., the embedding status watch in the frontend).
TaskEvent TypeDebounceCapacityPurpose
EmbeddingTaskEmbeddingEventConfigurable256Index page content into embedding vectors

Scheduled tasks implement the ScheduledTask trait:

pub trait ScheduledTask: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn interval(&self) -> Duration;
fn initial_delay(&self) -> Option<Duration>; // default: None
fn jitter_percent(&self) -> Option<u8>; // default: None
fn run(&self) -> impl Future<Output = Result<(), TaskError>> + Send;
}

Key behaviors:

  • Fixed interval: Uses tokio::time::interval_at for periodic execution.
  • Initial delay: Optional delay before the first tick, useful for letting the application stabilize after workspace open.
  • Jitter: Optional deterministic jitter (percentage of interval) applied to the first tick only. Prevents thundering-herd when multiple scheduled tasks share the same interval. The jitter is deterministic per task name (FNV-1a hash), so restarts produce the same offset.
TaskIntervalPurpose
HistoryCollapseTask24 hoursPrune event_log entries older than event_log_retention_days setting

Every registered task maintains a TaskHealth snapshot:

pub struct TaskHealth {
pub name: &'static str,
pub status: TaskStatus, // Idle | Running | Queued(usize) | Error(String)
pub last_run: Option<SystemTime>,
pub error_count: u64,
pub queue_depth: usize,
}

The TaskRunner::runner_health() method returns an aggregate TaskRunnerHealth with task count, active count, and error count. This is exposed to the frontend via the get_task_runner_health Tauri command, which serializes health data as TaskHealthSnapshot (with Unix millisecond timestamps for cross-language compatibility).


Task errors use the TaskError enum:

  • ExecutionFailed { message } — generic task body failure.
  • ChannelClosed — the event channel was closed unexpectedly.
  • Timeout { duration_ms } — the task exceeded its allowed execution time.

Errors increment the task’s error_count and set status to TaskStatus::Error(message). The task loop continues running after errors — a single failure does not terminate the task. If a task’s tokio join handle fails (panic), the supervisor logs the error and increments the error count.


The TaskRunner uses a CancellationToken shared across all tasks. On shutdown:

  1. cancel() is called, signaling all tokio::select! loops to exit.
  2. The supervisor awaits all task join handles.
  3. A oneshot channel signals completion back to the caller via shutdown_and_wait().

The Tauri integration wraps this in a 5-second timeout. If the timeout elapses, the runner is dropped (which calls cancel() again via Drop), and in-flight work is abandoned.


  • Embedding System: The EmbeddingTask is the primary event-driven consumer
  • Event Log System: The HistoryCollapseTask prunes old event log entries
  • Tokio Background Worker Patterns: Design patterns applied in this implementation

Provides unified background task execution for event-driven and scheduled workloads. See also: Embedding System, Event Log System.

Was this page helpful?