# `StatifierOban.Timer.Worker`
[🔗](https://github.com/riddler/statifier_oban/blob/v0.17.3/lib/statifier_oban/timer/worker.ex#L1)

The Oban worker a `%Statifier.Effect.SendDelayed{}` becomes.

Uniqueness is the whole point of this module: jobs are unique on the
`{scope, ordinal}` dedup pair (`StatifierOban.Timer.DedupKey`), read off
the args at the top level. Re-executing the same drive after a crash
rebuilds a byte-identical pair - both components are pure fold state
(st-ADR-0059) - 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 at-least-once redelivery safe.

The unique window is every state over an infinite period: a timer that
already completed, was cancelled (sob-2hx.4's path), 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`
on purpose - a host moving its timers queue must not turn a replay into
a second job - and exclude the meta the delivery module rides on, so a
host reconfiguring its delivery does not turn a replay into a second
job either.

`perform/1` decodes the stored effect and hands it to the job's
`StatifierOban.Timer.Delivery` module (from the meta written at
schedule time; absent meta falls back to the documented default,
`StatifierOban.Timer.Delivery.Session`), which owes the execution-liveness
check st-ADR-0054 decision 4 requires before any fired event is fed
back. The outcomes map onto Oban states so each is observable on the
job row:

- delivered -> the job completes (`:ok`);
- the execution is not live -> the job cancels with
  `{:discarded, reason}` recorded, the spec 6.2 discard as data;
- the execution is not finished but cannot take the event now -> the
  delivery answers `{:snooze, seconds}`, `seconds` a positive integer,
  and the job snoozes: Oban reschedules it at least `seconds` later
  without spending a retry, and it stays a pending timer (ADR-0010).
  A snooze-shaped answer that is not that - zero, a negative count,
  an Oban period tuple - matches no clause and retries like a raise;
- an undecodable row cancels with `{:undecodable, reason}` - no number
  of retries makes a corrupt row decodable. The cancel is silent:
  nothing is delivered into the chart, no new event is spelled, and
  this package emits no event of its own. What a host sees is the job
  record, cancelled with `{:undecodable, reason}` in its recorded
  errors, and Oban's own job event for it (`[:oban, :job, :stop]`,
  state `:cancelled`, the same reason in its `:result`);
- 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 delivery module that cannot be resolved returns
  `{:error, {:invalid_delivery, _}}` and retries - an environment fact
  about the host's code, fixable by a deploy, unlike the row facts
  above. A raise or exit out of the delivery module retries the same
  way, per the behaviour's contract.

`timeout/1` returns the job's run-time bound: the config's
`:timer_timeout`, written into the job's meta by
`StatifierOban.Timer.schedule/3`, or `:infinity` for a job whose row
carries none. Oban enforces it from outside the job process; an
attempt that runs past it fails with `Oban.TimeoutError` and retries
while attempts remain, exactly as a raise out of the delivery module
does. A fired timer has no failure door, so a timed-out last attempt
discards the job the way any other exhausted one does.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
