StatifierOban.Invoke.Worker (StatifierOban v0.5.0)

Copy Markdown View Source

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/1 returns {:error, reason} -> the job retries with {:run_failed, reason} recorded - the work is idempotent on invoke_id by contract, so retrying is what at-least-once means; a raise or exit out of run/1 (or the delivery module) retries the same way;
  • the attempt that retries is the last one (attempt has reached max_attempts, so Oban will discard rather than retry) -> the same job outcome as above, plus StatifierOban.Invoke.Delivery.deliver_failure/3 on the way past, feeding error.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 delivers StatifierOban.Invoke.Delivery.deliver_failure/3 on the way past, through the same liveness-checked door, whenever the row still yields the two plain-string identity fields; a row whose scope or invoke_id are 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}. :detail is that reason, inspected.
  • "run_crashed" - the last attempt raised or exited. :detail is 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. :detail is 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.