# `StatifierOban.Config`
[🔗](https://github.com/riddler/statifier_oban/blob/v0.17.3/lib/statifier_oban/config.ex#L1)

Configuration for running Statifier effects on a host-supplied Oban
instance.

Per ADR-0002, this package never owns, starts, or names an Oban instance:
the host supplies its own instance's name and every public entry point in
this package takes it from here. There is no default - not even Oban's own
default name `Oban` - so a missing instance is a configuration error at
the call site, never a silent fallback into whatever instance happens to
be running.

Queues are the host's for the same reason (ADR-0002 fixes instance
ownership and says each job kind's queue travels here as a further
field). `:timers_queue` names the host queue that delayed-send timer
jobs target; it is required with no default, so a job never falls back
silently into a host's `:default` queue.

`:invoke_queue` names the host queue that invoke-handler jobs
(`StatifierOban.Invoke.Worker`) target. It is optional because a
timers-only host has no invoke jobs to queue - but it has no default
either: a host whose handlers are built on
`StatifierOban.Invoke.Handler` gets `{:error, {:missing_option,
:invoke_queue}}` from the first `perform/2` rather than a silent
fallback queue.

The delivery seams are the options with defaults, and each default is a
documented choice rather than a fallback: `:delivery` (the
execution-liveness seam fired timers go through,
`StatifierOban.Timer.Delivery`) and
`:invoke_delivery` (the seam a completed invoke's `done.invoke` goes
through, `StatifierOban.Invoke.Delivery`) both default to their
`Statifier.Session`-backed check, which is correct for any host running
sessions with the session id as scope. A host answering liveness from
its own execution store supplies its implementations here.

## Examples

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :statifier_timers)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :statifier_timers}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban)
    {:error, {:missing_option, :timers_queue}}

    iex> StatifierOban.Config.new([])
    {:error, {:missing_option, :oban}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, queue: :timers)
    {:error, {:unknown_options, [:queue]}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, delivery: MyApp.RunStore)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, delivery: MyApp.RunStore}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, delivery: "MyApp.RunStore")
    {:error, {:invalid_option, :delivery, "MyApp.RunStore"}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, invoke_queue: :statifier_invokes)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, invoke_queue: :statifier_invokes}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, invoke_queue: 42)
    {:error, {:invalid_option, :invoke_queue, 42}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, opaque_codec: MyApp.ArgsCodec)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, opaque_codec: MyApp.ArgsCodec}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, opaque_codec: "MyApp.ArgsCodec")
    {:error, {:invalid_option, :opaque_codec, "MyApp.ArgsCodec"}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, child_starter: MyApp.Starter)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, child_starter: MyApp.Starter}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, child_starter: "MyApp.Starter")
    {:error, {:invalid_option, :child_starter, "MyApp.Starter"}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, max_fan_out: 50)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, max_fan_out: 50}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, max_fan_out: 0)
    {:error, {:invalid_option, :max_fan_out, 0}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, invoke_timeout: 30_000)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, invoke_timeout: 30_000}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, timer_timeout: 0)
    {:error, {:invalid_option, :timer_timeout, 0}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, unresolved_handler: :cancel)
    {:ok, %StatifierOban.Config{oban: MyApp.Oban, timers_queue: :t, unresolved_handler: :cancel}}

    iex> StatifierOban.Config.new(oban: MyApp.Oban, timers_queue: :t, unresolved_handler: :park)
    {:error, {:invalid_option, :unresolved_handler, :park}}

## The `:opaque_codec` seam (ADR-0002 stance)

`:opaque_codec` is optional and defaults to `nil` - identity, today's
plain `t2b64` encoding, unchanged. A host that wants a transform over
the bytes of the four host-opaque job-arg fields (a timer's `data` and
`caller_context`, an invoke's `params` and `content`) names a module
implementing `StatifierOban.OpaqueTerm.Codec` here. As with `:oban`,
`:delivery`, and every other seam this module owns, there is no ambient
or application-env fallback: a host that wants the transform states it
in the keyword list handed to `new/1`, explicitly, every time.

## The fan-out options (ADR-0007)

`:child_starter` is the third seam and the fan-out half's own: the
module implementing `StatifierOban.Invoke.ChildStarter` that a child
start job calls to create child `i` of the invocation. It is optional
and defaults to `nil`, because a host with no `core.map` in any of its
charts has no children to start - and it has no default module either,
for the reason `:invoke_queue` has none: this package does not create
executions, so there is nothing here to fall back to. A fan-out on a
config without one is refused on the invocation's error route rather
than starting nothing silently.

`:max_fan_out` is the runtime cap ADR-0007 decision 8 sends to the
host, given a number here at last: a positive integer, defaulting to
`1_000`, checked by the fan-out **before the first child start**. A
fan-out wider than the cap starts no children at all and fails the
invocation on `error.communication.invoke.<invoke_id>` with the count
and the cap in `detail`. The default is the largest N the family's
records have reasoned about rather than a measurement of any
particular deployment; a host that measures its own raises or lowers
it here.

## The run-time bounds

`:invoke_timeout`, `:child_start_timeout` and `:timer_timeout` bound
how long one attempt of each job kind may run: an invoke job
(`StatifierOban.Invoke.Worker`), a fan-out child start job
(`StatifierOban.Invoke.ChildStartWorker`) and a fired-timer job
(`StatifierOban.Timer.Worker`). Each is a positive integer of
milliseconds or `:infinity`, and each defaults to `:infinity` - no
bound, exactly what the workers did before the options existed.
A bound is capped by the largest timeout the BEAM accepts,
`4_294_967_295` ms (just under 50 days): `:child_start_timeout` and
`:timer_timeout` may be at most that, and `:invoke_timeout` at most
that less the invoke worker's 5000 ms backstop margin, `4_294_962_295`.
A larger integer is rejected as an invalid option.

A bound is fixed at enqueue time: the enqueue site writes it into the
job's meta, and the worker's `timeout/1` reads it back off the row, so
a job stored before a host changes its bound keeps the bound it was
stored with, and a job with no bound on its row runs unbounded. An
attempt that runs past its bound fails like any other failed attempt
and retries while attempts remain.

The invoke bound is also enforced inside the worker, around the
handler's `run/1` (or `run/2`): the call runs in a task linked to the
job process, and an attempt that outlives the bound fails with
`Oban.TimeoutError` raised from inside `perform/1`, so a timed-out
terminal attempt still delivers `error.communication.invoke.<invoke_id>`
with the `"run_crashed"` class (ADR-0005). `timeout/1` on that worker
returns the bound plus a fixed margin, as a backstop for the delivery
that follows the call. With the default `:infinity` no task is started
and `run/1` runs in the job process, as it always has.

## The unresolvable-handler policy

`:unresolved_handler` decides what an invoke job does when the handler
module named on its row does not resolve - renamed, removed, or simply
not yet deployed to this node. It takes `:retry` (the default) or
`:cancel`; any other value is rejected by `new/1`.

Under the default `:retry`, an unresolvable handler behaves exactly as
it always has: the attempt fails with `{:error, {:invalid_handler,
name}}` and retries while attempts remain, delivering nothing even on
exhaustion (`StatifierOban.Invoke.Worker`'s "Failure classes" section,
ADR-0005 decision 6).

Under `:cancel`, an unresolvable handler instead cancels the job on the
attempt that finds it, and delivers
`error.communication.invoke.<invoke_id>` with `"reason"`
`"invalid_handler"` and `"detail"` the handler name exactly as stored,
provided the job's delivery module itself resolves. An unresolvable
*delivery* module still retries under either value: there is no door
to deliver through. The policy is fixed at enqueue time, from the
config in force when the job was stored: `StatifierOban.Invoke.Handler`
writes it into the job's meta, and a job stored before the option
existed (or one with a hand-edited row) reads as `:retry`.

A host that deploys handler code in a separate release from the one
enqueueing invocations is what this exists for: rather than fighting a
retry backoff until the handler ships, `:cancel` parks the job and
tells the chart at once, so a transition on `error.communication` can
re-enqueue the invocation once the deploy lands.

# `t`

```elixir
@type t() :: %StatifierOban.Config{
  child_start_timeout: timeout_bound(),
  child_starter: module() | nil,
  delivery: module(),
  invoke_delivery: module(),
  invoke_queue: atom() | String.t() | nil,
  invoke_timeout: timeout_bound(),
  max_fan_out: pos_integer(),
  oban: Oban.name(),
  opaque_codec: module() | nil,
  timer_timeout: timeout_bound(),
  timers_queue: atom() | String.t(),
  unresolved_handler: unresolved_handler()
}
```

The host-supplied Oban configuration.

`:oban` is the name of the host's Oban instance, as given to
`Oban.start_link/1` - anything `t:Oban.name/0` allows. `:timers_queue`
is the host queue delayed-send timer jobs are inserted into - an atom or
string, exactly as the host names it in its own Oban `:queues`.
`:delivery` is the module implementing `StatifierOban.Timer.Delivery`
that fired timer jobs go through. `:invoke_queue` is the host queue
invoke-handler jobs are inserted into (`nil` on a timers-only host), and
`:invoke_delivery` is the module implementing
`StatifierOban.Invoke.Delivery` that a completed invoke's `done.invoke`
goes back through. `:opaque_codec` is the module implementing
`StatifierOban.OpaqueTerm.Codec` that the two enqueue sites run the
host-opaque job-arg fields through (`nil` - the default - is the
identity encoding). `:child_starter` is the module implementing
`StatifierOban.Invoke.ChildStarter` that fan-out child start jobs
create their child through (`nil` on a host with no fan-out), and
`:max_fan_out` is the positive integer cap on a fan-out's width.
`:invoke_timeout`, `:child_start_timeout` and `:timer_timeout` are the
per-attempt run-time bounds, in milliseconds or `:infinity`.
`:unresolved_handler` is `:retry` (the default) or `:cancel` - what an
invoke job does when its handler module does not resolve (see the
moduledoc's unresolvable-handler policy section).

# `timeout_bound`

```elixir
@type timeout_bound() :: pos_integer() | :infinity
```

A per-attempt run-time bound: milliseconds, or `:infinity` for none.

# `unresolved_handler`

```elixir
@type unresolved_handler() :: :retry | :cancel
```

The unresolvable-handler policy: `:retry` (the default) retries an
unresolvable handler to exhaustion and delivers nothing; `:cancel`
cancels the job on the attempt that finds it and delivers
`error.communication.invoke.<invoke_id>` with `"reason"`
`"invalid_handler"`.

# `new`

```elixir
@spec new(keyword()) :: {:ok, t()} | {:error, term()}
```

Builds a config from the host's options.

`:oban` and `:timers_queue` are required; `:invoke_queue` is optional
with no default (see the moduledoc); `:delivery` and `:invoke_delivery`
are optional and default to the `Statifier.Session`-backed seams;
`:opaque_codec` is optional and defaults to `nil` (identity - see the
moduledoc's ADR-0002 stance); `:child_starter` is optional and defaults
to `nil`, and `:max_fan_out` is optional and defaults to `1_000` (see
the moduledoc's fan-out section). `:invoke_timeout`,
`:child_start_timeout` and `:timer_timeout` are optional and default
to `:infinity` (see the moduledoc's run-time bounds section).
`:unresolved_handler` is optional and defaults to `:retry` - the other
accepted value is `:cancel` (see the moduledoc's unresolvable-handler
policy section). Unknown options are rejected rather than ignored, so
a typo fails loudly instead of silently dropping a setting.

---

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