Task Runner System
Status: Implemented Depends On: Tokio runtime Crate: crates/infrastructure/task-runner/
Overview
Section titled “Overview”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.
Architecture
Section titled “Architecture”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)Lifecycle
Section titled “Lifecycle”- Workspace open: A new
TaskRunneris created inopen_workspace_async(). Tasks are registered beforestart()is called. - 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.
- Workspace close: The runner is taken from
AppState.managers.task_runnerandshutdown_and_wait()is called with a bounded timeout (5 seconds). TheCancellationTokensignals all tasks to stop. - App exit: The same shutdown sequence runs in the Tauri
on_event(CloseRequested)handler. If the runner is dropped without explicit shutdown, theDropimpl cancels all tasks (fire-and-forget).
Integration with AppState
Section titled “Integration with AppState”AppState └── managers: ManagerBundle └── task_runner: Mutex<Option<TaskRunner>> Some(_) when workspace is open None when no workspace is activeThe get_task_runner_health Tauri command exposes per-task health snapshots to the frontend for diagnostics.
Event-Driven Tasks
Section titled “Event-Driven Tasks”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::channelwith configurable capacity.EventTaskHandle::try_send()returnsTaskSendError::Fullwhen 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
Vectohandle_batch(). This amortizes overhead for bursty workloads. - Debounce with deduplication: When
debounce_interval()returnsSome(duration), events accumulate in aHashSet(deduplicating byEq + Hash) and are flushed only after the debounce timer expires. The timer resets on each new event. TheSleepfuture is allocated once and reset in-place viaSleep::reset()to avoid per-event allocation. - Lock-free queue depth: An
AtomicUsizecounter tracks queue depth on the hottry_sendpath without acquiring the health mutex. - Status broadcasting: A
watch::channelprovidesTaskStatusupdates that consumers can subscribe to (e.g., the embedding status watch in the frontend).
Current Event-Driven Tasks
Section titled “Current Event-Driven Tasks”| Task | Event Type | Debounce | Capacity | Purpose |
|---|---|---|---|---|
EmbeddingTask | EmbeddingEvent | Configurable | 256 | Index page content into embedding vectors |
Scheduled Tasks
Section titled “Scheduled Tasks”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_atfor 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.
Current Scheduled Tasks
Section titled “Current Scheduled Tasks”| Task | Interval | Purpose |
|---|---|---|
HistoryCollapseTask | 24 hours | Prune event_log entries older than event_log_retention_days setting |
Health Monitoring
Section titled “Health Monitoring”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).
Error Handling
Section titled “Error Handling”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.
Shutdown Behavior
Section titled “Shutdown Behavior”The TaskRunner uses a CancellationToken shared across all tasks. On shutdown:
cancel()is called, signaling alltokio::select!loops to exit.- The supervisor awaits all task join handles.
- A
oneshotchannel signals completion back to the caller viashutdown_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.
Related
Section titled “Related”- Embedding System: The
EmbeddingTaskis the primary event-driven consumer - Event Log System: The
HistoryCollapseTaskprunes 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?
Thanks for your feedback!