Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,18 @@ impl TypedTask for SyncTask {
}
```

## Dependency failure policy

When a task declares dependencies via `.depends_on()`, you can configure what happens if a dependency fails permanently. The default is `Cancel`.

| Variant | Behavior |
|---------|----------|
| `Cancel` (default) | The dependent task is moved to history with `DependencyFailed` status. Other dependents in the chain are also cascade-cancelled. |
| `Fail` | The dependent is moved to history as `DependencyFailed`, but other dependents in the chain are not affected (for manual intervention). |
| `Ignore` | The dependent is unblocked and runs anyway. The executor must handle missing upstream results. |

Set per-submission with `.on_dependency_failure(DependencyFailurePolicy::Ignore)`. There is no global builder option — the default `Cancel` is appropriate for most use cases.

## Application state

Executors often need shared services (HTTP clients, database connections, caches). Rather than capturing `Arc<T>` per executor, register state on the builder:
Expand Down
4 changes: 4 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ Quick reference for terms used throughout the taskmill documentation.

| Term | Definition |
|------|------------|
| **Blocked** | A task status indicating the task has unresolved dependencies and cannot be dispatched. Blocked tasks transition to `pending` when all their dependencies complete successfully, or to `DependencyFailed` if a dependency fails (depending on the failure policy). See [Quick Start](quick-start.md#task-dependencies). |
| **Backpressure** | Slowing down new work when the system is already busy. Taskmill uses [pressure sources](io-and-backpressure.md#backpressure-external-pressure-signals) to detect load and [throttle policies](priorities-and-preemption.md#throttle-behavior) to decide which tasks to defer. |
| **Delayed task** | A task with a `run_after` timestamp that defers dispatch until that time arrives. If the timestamp is in the past (e.g., after a restart), the task runs immediately. See [Quick Start](quick-start.md#delayed-tasks). |
| **Dependency edge** | A record in the `task_deps` junction table representing that one task depends on another. An edge `(A, B)` means "task A cannot start until task B completes." Edges are removed when the dependency completes or fails, and are cleaned up on startup if they reference tasks that no longer exist. |
| **DependencyFailurePolicy** | Controls what happens to a dependent task when one of its dependencies fails permanently. `Cancel` (default) moves the dependent to history as `DependencyFailed` and cascades to other dependents. `Fail` does the same without cascading. `Ignore` unblocks the dependent anyway. See [Configuration](configuration.md#dependency-failure-policy). |
| **Deduplication (dedup)** | Preventing the same task from being queued twice. Taskmill generates a SHA-256 key from the task type and payload; a second submission with the same key is silently ignored. See [Persistence & Recovery](persistence-and-recovery.md#deduplication). |
| **Dispatch** | Moving a task from "waiting in line" (pending) to "actively running." The scheduler dispatches tasks in priority order, subject to concurrency limits and backpressure. |
| **EWMA** | Exponentially Weighted Moving Average — a smoothing technique that gives recent measurements more weight than old ones. Taskmill uses EWMA to smooth resource readings so the scheduler doesn't overreact to momentary spikes. See [IO & Backpressure](io-and-backpressure.md#ewma-smoothing). |
Expand All @@ -15,6 +18,7 @@ Quick reference for terms used throughout the taskmill documentation.
| **Preemption** | Pausing lower-priority work so higher-priority work can run immediately. Preempted tasks resume automatically once the urgent work finishes. See [Priorities & Preemption](priorities-and-preemption.md#preemption). |
| **Pressure source** | Anything that signals the system is busy — disk IO, network throughput, memory usage, API rate limits, battery level. Returns a value from 0.0 (idle) to 1.0 (saturated). See [IO & Backpressure](io-and-backpressure.md#pressure-sources). |
| **Task group** | A named set of tasks that share a concurrency limit. For example, you might limit uploads to a specific S3 bucket to 3 at a time. See [Priorities & Preemption](priorities-and-preemption.md#task-groups). |
| **task_deps** | The SQLite junction table that stores dependency edges between tasks. Each row `(task_id, depends_on_id)` means the task cannot start until the dependency completes. Edges survive restarts and are cleaned up automatically when dependencies resolve or on startup. See [Persistence & Recovery](persistence-and-recovery.md#dependency-recovery). |
| **Throttle policy** | Rules that map system pressure to dispatch decisions. The default policy defers background tasks when pressure exceeds 50% and normal tasks when it exceeds 75%, but never blocks high-priority work. See [Priorities & Preemption](priorities-and-preemption.md#throttle-behavior). |
| **TTL (time-to-live)** | A duration after which a task automatically expires if it hasn't started running. Configurable per-task, per-type, or as a global default. See [Configuration](configuration.md#task-ttl-time-to-live). |
| **TtlFrom** | Controls when the TTL clock starts: `Submission` (at submit time, the default) or `FirstAttempt` (when the task is first dispatched). See [Configuration](configuration.md#task-ttl-time-to-live). |
Expand Down
10 changes: 10 additions & 0 deletions docs/persistence-and-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ Delayed and recurring tasks are fully persistent and survive restarts:
- **Missed delayed tasks run immediately** — if a task's `run_after` timestamp is in the past when the scheduler starts (e.g., the app was offline), the task is dispatched on the first cycle rather than being silently dropped.
- **Running recurring instances are reset to pending** — this is the same behavior as all running tasks during crash recovery. The crash does not count as a retry, and the recurring schedule continues normally after the re-run completes.

## Dependency recovery

Task dependency edges (stored in the `task_deps` table) are fully persisted and survive restarts.

- **Blocked tasks stay blocked.** Their edges are in `task_deps` and resolution happens normally when their dependencies complete.
- **Running dependencies are reset to pending.** This is the standard crash recovery behavior for all running tasks. Once the reset dependency re-executes and completes, its dependents are unblocked as usual.
- **Stale edge cleanup on startup.** During recovery, the scheduler deletes any edges in `task_deps` that point to tasks no longer in the active queue (e.g., if a cancellation was interrupted mid-operation). Any blocked tasks left with zero remaining edges are then transitioned to `pending`.

No manual intervention is needed — dependency chains resume correctly after any restart or crash.

## Deduplication

A common problem: your app submits "upload photo.jpg" twice because the user clicked a button while a sync was already running. Without dedup, you'd upload the same file twice.
Expand Down
3 changes: 3 additions & 0 deletions docs/progress-and-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ tokio::spawn(async move {
| `TaskExpired { header, age }` | Task expired (TTL exceeded) — `age` is the time since the TTL clock started |
| `RecurringSkipped { header, reason }` | A recurring instance was skipped (e.g., pile-up prevention) |
| `RecurringCompleted { header, occurrences }` | A recurring schedule finished all its occurrences |
| `TaskUnblocked { task_id }` | A blocked task's dependencies are all satisfied — it transitions to `pending` |
| `DependencyFailed { task_id, failed_dependency }` | A blocked task was cancelled because a dependency failed permanently |
| `Paused` | Scheduler globally paused via `pause_all()` |
| `Resumed` | Scheduler resumed via `resume_all()` |

Expand All @@ -103,6 +105,7 @@ Task-specific events share a `TaskEventHeader` with `task_id`, `task_type`, `key
| Upload status indicators | `Dispatched`, `Progress`, `Completed`, `Failed`, `Preempted` |
| Stale task cleanup UI | `TaskExpired` |
| Recurring schedule monitoring | `RecurringSkipped`, `RecurringCompleted` |
| Dependency chain tracking | `TaskUnblocked`, `DependencyFailed` |

## Querying progress

Expand Down
9 changes: 9 additions & 0 deletions docs/query-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ Use these queries to build dashboards, debug stuck tasks, and gather analytics a
| `task_by_key(key)` | `Option<TaskRecord>` | Look up an active task by dedup key. |
| `running_io_totals()` | `(i64, i64)` | Sum of expected disk read and write bytes across running tasks. Useful for comparing against system capacity. |

## Dependency queries

| Method | Returns | Description |
|--------|---------|-------------|
| `task_dependencies(id)` | `Vec<i64>` | IDs of tasks that this task depends on (its prerequisites). |
| `task_dependents(id)` | `Vec<i64>` | IDs of tasks that depend on this task (will be unblocked when it completes). |
| `blocked_tasks()` | `Vec<TaskRecord>` | All tasks currently in `blocked` status, waiting for dependencies. |
| `blocked_count()` | `i64` | Count of blocked tasks. Also available in `SchedulerSnapshot::blocked_count`. |

## History queries

| Method | Returns | Description |
Expand Down
57 changes: 57 additions & 0 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,63 @@ scheduler.resume_recurring(task_id).await?;
scheduler.cancel_recurring(task_id).await?;
```

## Task dependencies

Tasks can declare dependencies on other tasks. A dependent task stays in `blocked` status and won't be dispatched until all its dependencies have completed successfully.

### Simple chain

```rust
let upload = scheduler.submit(
&TaskSubmission::new("upload-file")
.payload_json(&upload_plan)
).await?;

// Only runs after upload succeeds
scheduler.submit(
&TaskSubmission::new("delete-old-version")
.depends_on(upload.id().unwrap())
.payload_json(&delete_plan)
).await?;
```

### Fan-in (multiple dependencies)

Use `.depends_on_all()` when a task needs several prerequisites to complete first:

```rust
let a = scheduler.submit(&TaskSubmission::new("fetch-a").payload_json(&a_data)).await?;
let b = scheduler.submit(&TaskSubmission::new("fetch-b").payload_json(&b_data)).await?;

// Only runs after both A and B complete
scheduler.submit(
&TaskSubmission::new("merge")
.depends_on_all([a.id().unwrap(), b.id().unwrap()])
.payload_json(&merge_plan)
).await?;
```

### Failure handling

By default, if a dependency fails permanently, the dependent task is cancelled and recorded as `DependencyFailed` in history. This is the `Cancel` policy. You can change this per-submission:

```rust
use taskmill::DependencyFailurePolicy;

scheduler.submit(
&TaskSubmission::new("cleanup")
.depends_on(upload_id)
.on_dependency_failure(DependencyFailurePolicy::Ignore) // run anyway
.payload_json(&cleanup_plan)
).await?;
```

| Policy | Behavior |
|--------|----------|
| `Cancel` (default) | Dependent is moved to history as `DependencyFailed`. |
| `Fail` | Same as `Cancel`, but doesn't cascade to other dependents in the chain. |
| `Ignore` | Dependent is unblocked and runs anyway — your executor must handle missing upstream results. |

## Tauri integration

Taskmill is designed for Tauri. The `Scheduler` drops directly into Tauri state, and all events are serializable for IPC.
Expand Down
22 changes: 22 additions & 0 deletions migrations/006_dependencies.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Junction table for task dependency edges.
-- A row (task_id=A, depends_on_id=B) means "A cannot start until B completes."
CREATE TABLE IF NOT EXISTS task_deps (
task_id INTEGER NOT NULL,
depends_on_id INTEGER NOT NULL,
PRIMARY KEY (task_id, depends_on_id)
);

-- Index for the "who depends on me?" query (used when a task completes).
CREATE INDEX IF NOT EXISTS idx_task_deps_depends_on
ON task_deps (depends_on_id);

-- New status value: 'blocked' is now valid for tasks.status.
-- No schema change needed — status is TEXT, not an enum.
-- Add partial index for blocked tasks.
CREATE INDEX IF NOT EXISTS idx_tasks_blocked
ON tasks (status)
WHERE status = 'blocked';

-- Column to store the dependency failure policy for blocked tasks.
-- Values: 'cancel' (default), 'fail', 'ignore'.
ALTER TABLE tasks ADD COLUMN on_dep_failure TEXT NOT NULL DEFAULT 'cancel';
Loading
Loading