The host-injected Oban worker (ADR-0004): the consuming app defines one
module, and the Oban beam compiles only where Oban exists. This macro
expands use Oban.Worker inside the host's compilation, so the package
itself never references a loaded Oban module and compiles Oban-free
(the CI no-optional leg's proof).
defmodule MyApp.WebhookDeliveryWorker do
use AshHooks.Worker,
deliveries: MyApp.OutboundDelivery,
endpoints: MyApp.WebhookEndpoint,
secret_resolver: {MyApp.Secrets, :webhook_secret},
queue: :webhooks,
oban: MyApp.Oban
endConsumers pass enqueue: {MyApp.WebhookDeliveryWorker, :enqueue} to
AshHooks.dispatch/4; the generated enqueue/2 is that seam.
Options:
:deliveries,:endpoints(required) — the consumer's resource modules carrying theAshHooks.OutboundDelivery/AshHooks.Endpointextensions.:secret_resolver(required,{m, f}) — resolves an endpoint's secret reference:f(ref) :: {:ok, secret_binary} | {:error, term}. The returned value signs the Standard Webhooks envelope (itswhsk_/whsec_prefix only selects the key slot for rotation); legacy envelopes, when the signing mode uses them, are signed from the endpoint'slegacy_secret_ref/legacy_previous_secret_refreferences through this same resolver.:tenant_aware_secrets(optional, defaultfalse) — switches the resolver contract to 2-arity:f(ref, tenant). The tenant is the run's args-threaded tenant (the delivery row's tenant, serialized at enqueue). Explicit, no arity magic: the default 1-arity contract is unchanged.:snippet_redactor({m, f}, optional) — a consumer callback run on the raw captured body ahead of the package's snippet floor (domain-specific tokens need raw input). Only consulted on per-callsnippet_capture: truediagnostic runs; a crash or invalid return degrades to the sanitized summary, never raw bytes. The capture flag itself is deliberately not a macro option (ADR-0005's snippet amendment: compile-time knobs are broad and quiet) — pass it in theAshHooks.Delivery.run/2config for a one-row diagnostic re-drive.:http— theAshHooks.Httpadapter (defaultAshHooks.Http.Bounded).:http_opts— adapter options as a literal keyword list or a{module, function, args}callback resolved inside each job's monitored delivery deadline.:oban— the Oban instance name (default the unnamed instance).:queue(default:ash_hooks),:timeout(35,000 ms), and:max_attempts(20) — Oban Worker options. The job timeout must be greater than:attempt_timeout + :finalization_allowance.:attempt_timeout(25,000 ms) and:finalization_allowance(5,000 ms) — the driver deadline and extra send-lease interval. The attempt budget includes endpoint lookup, secret resolution, signing, destination and DNS checks, HTTP, and result persistence. Keep application clocks synchronized because lease comparisons use the application clock.:delivery_max_attempts(default 10),:base_backoff_seconds(2),:max_backoff_seconds(3600),:retry_after_cap_seconds(86_400) — the row-driven retry policy. For directAshHooks.Delivery.run/2calls,:delivery_max_attemptsbecomes:max_attempts; the other retry option names and defaults are unchanged. Omitted ornilretry options use these defaults. TheRetry-Aftercap is receiver-held-state budget: a receiver returning a largeRetry-Afterholds its delivery row and its Oban job for up to the cap per attempt (the 86,400 default = up to 24 hours per attempt on one header; a receiver honoringRetry-Afterdeliberately asks for exactly that). Lower the cap when the chosen posture is exhaust-fast (a wedged receiver dead-letters at the ceiling instead of holding state). The cap clampsRetry-After; it does not alter the backoff ladder.
The generated enqueue uses fields: [:args] with the complete delivery
key, delivery and endpoint resource names, source, route, endpoint/event,
and tenant keys. Uniqueness has an infinite period over runnable states
(available, scheduled, executing, and retryable). Completed,
canceled, and discarded jobs therefore permit a later recovery trigger.
Enqueue succeeds only after Oban returns a persisted runnable job whose
identity args match the request. Oban.Basic may briefly report a uniqueness
conflict without the winning job's ID while its advisory-lock peer commits;
the generated path retries that result for at most twenty 5 ms waits, then
returns {:error, :job_not_persisted}. Before insertion it binds a deferred
route once with a compare-and-set update; a different stored route returns
{:error, :dispatch_route_conflict}.
On multitenant deliveries the enqueue also serializes the row's tenant
into job args (the attribute value inverted through the resource's
tenant_from_attribute, so Delivery.run/2's forward
parse_attribute round-trips for non-identity parsers too); the
uniqueness identity includes the serialized tenant. The
tenant must round-trip Oban's JSON encoding — string tenants (uuids,
slugs) are the supported shape; adopters with a custom parse_attribute
pair it with tenant_from_attribute (the default inverse is identity).