# `SuperPi.Durable.TaskEngine`
[🔗](https://github.com/oleksandr/super-pi-beam/blob/v0.1.0/lib/super_pi/durable/task_engine.ex#L1)

Durable task state machine scheduler (`REQ-FSM-01`..`REQ-FSM-05`, `REQ-OBAN-01`..`REQ-OBAN-02`).

Uses `Fsmx` inside `Ecto.Multi` transactions to enforce valid state transitions:
- `pending` -> `running` | `aborted` | `cancelled` | `orphaned` | `failed`
- `running` -> `waiting` (`all_settled` | `fail_fast`) | `sleeping` (`sleep_until`) | `completing` | `completed` | `failed` | `aborted`
- Open-time recovery (`reconcile_on_open!/1`): resets surviving `running` tasks to `pending`,
  increments `attempt`, cancels tasks with `abandon_on_restart: true`, and fails tasks that
  exceed `restart_limit` (default `5`).
- Bottom-up subtree abort (`abort_task!/1`): marks the target task and its foreground-owned
  children `abort_requested: true`, settling children before completing the parent's abort.
- A parent in `waiting` that a settled child wakes (`pending`) is announced to its conversation's
  machine, on any node, once the transaction has committed (`{:task_woken, task_id}`). A machine
  that is gone finds the woken task when it next opens.

# `abort_task!`

Bottom-up subtree abort (`spec.md` §5.4):
Marks the target task and all non-background owned descendants `abort_requested: true`.
Settles leaf descendants as `"aborted"` first, then settles the parent task.

# `cancel_task`

Cancels a `"running"` or `"sleeping"` task with `result` (a user cancel). Live children are
aborted and a waiting parent is reconciled, as for `fail_task/2`. A sleeping task's wake job
finds it no longer sleeping and does nothing.

# `checkpoint_task`

Checkpoints a running task to a new phase (`{phase: next_phase, ...}`).

# `complete_task`

Completes a task with `result`.
- If the task still has live foreground owned child tasks, transitions to `"completing"` and holds `result`.
- Otherwise transitions to `"completed"` and reconciles any parent task waiting on it.

# `create_task`

Creates a new durable task in `"pending"` status.
Rejects creation if `owner_task_id` is already `completing`, terminal, or `abort_requested`.

# `fail_task`

Fails a task with `error_map` and triggers `fail_fast` or `all_settled` reconciliation on its parent.

# `get_task`

The task as stored, or `nil`.

# `get_tasks`

The tasks with these ids, in id order.

# `live_children`

The foreground tasks that `task_id` owns and that have not settled.

# `next_pending_generation`

The oldest pending `pi.generation` task of the conversation, or `nil`. A machine that starts
after a crash or a failover resumes it (see `Machine`, `:reconcile`).

# `next_waiting_generation`

The oldest `pi.generation` task of the conversation that waits on its children (`waiting`), or
`nil`. A machine that starts while a turn is parked resumes the park (see `Machine`, `:reconcile`).

# `reconcile_on_open!`

Open-time reconciliation (`spec.md` §2.5, §5.4, `REQ-FSM-04`, `REQ-FSM-05`):
- Resets surviving `"running"` tasks for `conversation_id` to `"pending"` and increments `attempt`.
- If `abandon_on_restart: true`, settles the task as `"cancelled"`.
- If `attempt > restart_limit`, settles the task as `"failed"` with `"restart_limit_exceeded"`.

# `sleep_until`

Transitions a running task to `"sleeping"` until `wake_at` (`DateTime.t()`) and schedules an
`Oban` wake job (`REQ-OBAN-01`).

# `start_task`

Atomically transitions a task to `"running"`. The task row is locked for the
read and the write, so two workers cannot both claim it: the loser gets
`{:error, :already_running}`.

# `transition_task`

Performs an atomic `Fsmx` transition under a row lock (`REQ-FSM-01`, `REQ-FSM-02`).
Returns `{:ok, task}`, `{:error, :not_found}` or `{:error, changeset}`.

# `wait_for_children`

Transitions a running task to `"waiting"` on `child_task_ids` with policy `"all_settled"` or `"fail_fast"`.

The parent row is locked while the children are read and the parent is
written. A child that settles concurrently takes the same lock in
`reconcile_parent_after_child_settled!/2`, so it either sees `"waiting"` and
wakes the parent, or commits first and this call sees it settled. No lost wakeup.

Options: `:phase` and `:state` are checkpointed together with the move to `"waiting"`. The
task is no longer `running` afterwards, so a later `checkpoint_task/3` would be refused.

# `wake_sleeping_task`

Wakes a `"sleeping"` task back to `"pending"` when its deadline arrives.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
