Baton.Completion (Baton v0.27.4)

Copy Markdown View Source

Detects when a whole workflow has finished and announces it exactly once.

Two paths feed this module:

  • check/2 — called by Baton.Worker every time a step reaches a terminal state. When that step is the last one outstanding, the workflow has settled. This is the prompt path: the notification fires the instant the final step returns.

  • announce/2 — called by Baton.Plugin's sweep as a backstop, for workflows that settled without a clean worker return (e.g. a step that hard-crashed or was killed by Oban, so check/2 never ran for it).

Exactly-once

Both paths funnel through a single atomic claim: the first writer to insert the workflow's row in workflow_completions (on-conflict-do-nothing) is the one that broadcasts {:workflow_finished, _} and emits [:baton, :workflow, :finished]. Everyone else — a retry, a simultaneous sibling step, or the plugin arriving after the worker already announced — sees the conflict and stays silent. So the terminal notification fires once per workflow, regardless of which path detects it.

Why the just-finished step is passed to check/2

When a worker's perform/1 returns, Oban hasn't yet written the new terminal state to oban_jobs — at that instant the current job still reads as executing. So check/2 is told the step's resolved state explicitly and overlays it on what the database reports. announce/2 has no such lag (the plugin only sees states Oban has already written), so it reads straight from the database.

Summary

Types

What status/1 can report about a workflow: settled with an outcome and the steps that failed, still running, or unknown to baton.

Functions

Backstop path for Baton.Plugin: announce a workflow as finished if its database state shows it fully settled and it hasn't been announced yet.

Given a job that just reached terminal_state, broadcast a finished event if the workflow is now fully settled and not already announced. Returns :ok regardless; failures here are logged and never propagated to the job.

Report a workflow's settled state by reading it, with no announcement and no side effects.

Types

status()

@type status() ::
  {:finished, :completed | :failed, [String.t()]} | :running | :unknown

What status/1 can report about a workflow: settled with an outcome and the steps that failed, still running, or unknown to baton.

Functions

announce(workflow_id, label)

@spec announce(String.t(), String.t() | nil) ::
  {:finished, :completed | :failed} | :noop

Backstop path for Baton.Plugin: announce a workflow as finished if its database state shows it fully settled and it hasn't been announced yet.

Returns {:finished, outcome} if this call made the announcement, or :noop if the workflow is still running or was already announced.

check(job, terminal_state)

@spec check(Oban.Job.t(), String.t()) :: :ok

Given a job that just reached terminal_state, broadcast a finished event if the workflow is now fully settled and not already announced. Returns :ok regardless; failures here are logged and never propagated to the job.

status(workflow_id)

@spec status(String.t()) :: status()

Report a workflow's settled state by reading it, with no announcement and no side effects.

The {:workflow_finished, _} broadcast fires exactly once and is not replayed: a subscriber that is down, not yet started, or on a node that wasn't listening at that instant misses it permanently, because the workflow_completions claim stops any later path from announcing again. This is the catch-up route for those subscribers — a host can sweep its own still-open run records and settle any whose workflow has, in fact, finished.

Returns {:finished, outcome, failed_steps} for a settled workflow, :running while any step is still pending, and :unknown for a workflow baton has no steps and no completion row for (never inserted, or fully pruned by Baton.Retention).

The recorded outcome wins when a completion row exists, so what this reports matches what was broadcast even after retention has pruned the steps behind it — in which case failed_steps comes back empty, since the per-step detail is gone.