Detects when a whole workflow has finished and announces it exactly once.
Two paths feed this module:
check/2— called byBaton.Workerevery 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 byBaton.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, socheck/2never 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
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
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.
Returns {:finished, outcome} if this call made the announcement, or
:noop if the workflow is still running or was already announced.
@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.
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.