The Oban worker a base-handler invocation becomes.
Uniqueness is the whole point of this module, exactly as it is for
StatifierOban.Timer.Worker: jobs are unique on the
{scope, invoke_id, macrostep} triple (ADR-0003), read off the args
at the top level. Re-executing the same drive after a crash rebuilds
a byte-identical triple - invoke_id is either the author's literal
id, used verbatim, or a deterministic %MachineState{} counter
(st-ADR-0008 as amended), and macrostep is pure fold state stamped
on the effect - so the duplicate insert conflicts with the stored job
and becomes a no-op. That conflict, not any check in host code, is
what makes the at-least-once perform/2 contract (st-ADR-0051
decision 4) safe for the enqueue itself.
macrostep is in the key because invoke_id alone cannot tell a
crash replay from a state re-entry: an authored id (<invoke id="resolve">) is byte-identical on every re-entry of its state, and
a retry loop that re-enters legitimately schedules a fresh
invocation. Invocations start only at the end of the macrostep, for
states still active then, so a {state, invoke_index} pair invokes
at most once per macrostep: within one macrostep the triple collides
exactly when the insert is a replay of the same scheduling decision,
and across macrosteps it never collides at all (ADR-0003 lays this
out).
The unique window is every state over an infinite period: an invoke
whose job already completed, was cancelled, or was discarded must
still swallow a replayed insert, because the replay is the same
scheduling decision, not a new one. The unique fields exclude :queue
and the meta the delivery module rides on, for the same reasons the
timer worker's do. Cancellation never carries a macrostep: it
addresses {scope, invoke_id} across every generation, the same way
timer cancellation addresses every row under a send_id.
perform/1 decodes the stored effect, resolves the handler module the
args carry, calls its run/2 (or its run/1, for a handler that
defines only that arity) - the host's actual work, at least once,
idempotent on invoke_id by that module's own contract - and hands
the result to the job's StatifierOban.Invoke.Delivery module (from
the meta written at enqueue time; absent meta falls back to the
documented default, StatifierOban.Invoke.Delivery.Session), which
owes the run-liveness check before any completion is fed back. The
outcomes map onto Oban states so each is observable on the job row:
- work done and delivered -> the job completes (
:ok); - the run is not live -> the job cancels with
{:discarded, reason}recorded - a completed invoke against a dead or halted run is discarded the same way a fired timer is; run/1returns{:error, reason}-> the job retries with{:run_failed, reason}recorded - the work is idempotent oninvoke_idby contract, so retrying is what at-least-once means; a raise or exit out ofrun/1(or the delivery module) retries the same way;- the attempt that retries is the last one (
attempthas reachedmax_attempts, so Oban will discard rather than retry) -> the same job outcome as above, plusStatifierOban.Invoke.Delivery.deliver_failure/3on the way past, feedingerror.communication.invoke.<invoke_id>into the run behind the same liveness check a completion goes through (st-ADR-0068, ADR-0005); - an undecodable row cancels with
{:undecodable, reason}- no number of retries makes a corrupt row decodable - and deliversStatifierOban.Invoke.Delivery.deliver_failure/3on the way past, through the same liveness-checked door, whenever the row still yields the two plain-string identity fields; a row whosescopeorinvoke_idare themselves undecodable names nobody to tell, so it cancels with no delivery, exactly as it always did; - a codec named on the row that this node cannot resolve, or one that
cannot decode the row right now, returns
{:error, {:invalid_codec, _}}or{:error, {:codec_failed, _}}and retries - an environment fact, fixable by a deploy or by making the key available, not a fact about the row; - a handler or delivery module that cannot be resolved returns
{:error, {:invalid_handler, _}}/{:error, {:invalid_delivery, _}}and retries - environment facts about the host's code, fixable by a deploy, unlike the row facts above.
Failure classes
The :reason string on a failure delivery is this package's
vocabulary to choose: st-ADR-0068 fixes the event and the payload
shape but interprets neither. Three classes are emitted - the two ways
run/1 can exhaust its retries, and the one way a job is over before
run/1 is ever reached:
"run_failed"- the last attempt returned{:error, reason}.:detailis that reason, inspected."run_crashed"- the last attempt raised or exited.:detailis the exception message, or the exit reason inspected."undecodable"- the stored row could not be rebuilt into an effect, so the job cancels rather than retrying.:detailis the typed decode error, inspected.
:attempts is the job's attempt on the try that gave up. For the
two run/1 classes that is the terminal attempt, which equals
max_attempts; for "undecodable" it is the attempt that found the
corrupt row, because that attempt cancels and there is no later one.
Of the failures that are not about the row, only run/1's own
exhaustion delivers. The environment errors above
(:invalid_handler, :invalid_delivery, :invalid_codec,
:codec_failed) retry and can in principle exhaust too, but they say
nothing about the invocation - they say the deploy is wrong - and
:invalid_delivery has by definition no seam to deliver through.
ADR-0005 records that limit rather than leaving it to be inferred.