SuperPi.Durable.TaskEngine (super_pi v0.1.0)

Copy Markdown View Source

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.

Summary

Functions

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.

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.

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

Completes a task with result.

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

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

The task as stored, or nil.

The tasks with these ids, in id order.

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

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).

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).

Open-time reconciliation (spec.md §2.5, §5.4, REQ-FSM-04, REQ-FSM-05)

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

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}.

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

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

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

Functions

abort_task!(task_id)

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(task_id, result \\ %{"reason" => "cancelled"})

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(task_id, next_phase, new_state)

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

complete_task(task_id, result \\ %{})

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(conversation_id, kind, input \\ %{}, opts \\ [])

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

fail_task(task_id, error_msg)

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

get_task(task_id)

The task as stored, or nil.

get_tasks(task_ids)

The tasks with these ids, in id order.

live_children(task_id)

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

next_pending_generation(conversation_id)

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(conversation_id)

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!(conversation_id)

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(task_id, wake_at, checkpoint_state \\ nil)

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

start_task(task_id)

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(task_id, target_status, extra_params \\ %{})

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(task_id, child_task_ids, wait_policy \\ "all_settled", opts \\ [])

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(task_id)

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