Get familiar with Ash resources
If you haven't already, read the Ash Getting Started Guide, and familiarize yourself with Ash and Ash resources.
Get familiar with AshOban Triggers & Scheduled Actions
See Triggers and Scheduled Actions to read
about what AshOban provides.
Bring in the ash_oban dependency
{:ash_oban, "~> 0.8.11"}Setup
Oban Pro
If you are using Oban Pro, set the following configuration:
config :ash_oban, :pro?, trueOban Pro lives in a separate hex repository, and therefore we, unfortunately, cannot have an explicit version dependency on it. What this means is that any version you use in hex will technically be accepted, and if you don't have the oban pro package installed and you use the above configuration, you will get compile time errors/warnings.
Using Igniter (recommended)
This will install oban as well.
mix igniter.install ash_obanManual
Next, allow AshOban to alter your configuration in your Application module:
# Replace this
{Oban, your_oban_config}
# With this
{Oban, AshOban.config(Application.fetch_env!(:my_app, :ash_domains), your_oban_config)}
# OR this, to selectively enable AshOban only for specific domains
{Oban, AshOban.config([YourDomain, YourOtherDomain], your_oban_config)}Usage
Warning
Currently, even without scheduler_cron specified, the triggers will run every minute. To disable this behavior, add scheduler_cron false. This will change with the next major release.
Finally, configure your triggers in your resources.
Add the AshOban extension and define a trigger.
For example:
defmodule MyApp.Resource do
use Ash.Resource, domain: MyDomain, extensions: [AshOban]
...
oban do
triggers do
# add a trigger called `:process`
trigger :process do
# this trigger calls the `process` action
action :process
# for any record that has `processed != true`
where expr(processed != true)
# checking for matches every minute
scheduler_cron "* * * * *"
on_error :errored
end
end
end
endMake sure to add the queue to the list of queues in Oban configuration.
Default queue is resources short name plus the name of the trigger.
For the above example you would add :resource_process queue to Oban queues in config.
Alternatively, you can define your own queue in the trigger.
See the DSL documentation for more: AshOban
Handling Errors
Error handling is done by adding an on_error to your trigger. This is an update action that will get the error as an argument called :error. The error will be an Ash error class. These error classes can contain many kinds of errors, so you will need to figure out handling specific errors on your own. Be sure to add the :error argument to the action if you want to receive the error.
This is not foolproof. You want to be sure that your on_error action is as simple as possible, because if an exception is raised during the on_error action, the oban job will fail. If you are relying on your on_error logic to alter the resource to make it no longer apply to a trigger, consider making your action do only that. Then you can add another trigger watching for things in an errored state to do more rich error handling behavior.
Triggering on action
Often you would need the trigger to activate when certain actions are performed, e.g. to expedite processing of new and updated records.
For that you can use AshOban.Changes.BuiltinChanges.run_oban_trigger
For example:
defmodule MyApp.Resource do
use Ash.Resource, domain: MyDomain, extensions: [AshOban]
...
oban do
triggers do
trigger :process do
action :process
where expr(processed != true)
end
end
end
create :create do
accept :*
change run_oban_trigger(:process)
end
endChanging Triggers when using Oban Pro
To remove or disable triggers, do not just remove them from your resource. Due to the way that Oban Pro implements cron jobs, if you just remove them from your resource, the cron will attempt to continue scheduling jobs. Instead, set state :paused or state :deleted on the trigger. See the oban docs for more: https://getoban.pro/docs/pro/0.14.1/Oban.Pro.Plugins.DynamicCron.html#module-using-and-configuring
PS: state :deleted is also idempotent, so there is no issue with deploying with that flag set to true multiple times. After you have deployed once with state :deleted you can safely delete the trigger.
When not using Oban Pro, all crons are simply loaded on boot time and there is no side effects to simply deleting an unused trigger.
Transactions
AshOban adds two new transaction reasons, as it uses explicit transactions to ensure that each triggered record is properly locked and executed in serially.
%{
type: :ash_oban_trigger,
metadata: %{
resource: Resource,
trigger: :trigger_name,
primary_key: %{primary_key_fields: value}
}
}and
%{
type: :ash_oban_trigger_error,
metadata: %{
resource: Resource
trigger: :trigger_name,
primary_key: %{primary_key_fields: value},
error: <the error (this will be an ash error class)>
}
}Authorizing actions
As of v0.2, authorize?: true is passed into every action that is called. This may be a breaking change for some users that are using policies. There are two ways to get around this:
- you can set
config :ash_oban, authorize?: false(easiest, reverts to old behavior, but not recommended) - you can install the bypass at the top of your policies in any resource that you have triggers on that has policies:
policies do
bypass AshOban.Checks.AshObanInteraction do
authorize_if always()
end
...the rest of your policies
endShared Context
By default, context set by AshOban (like ash_oban?: true and the %Oban.Job{} struct) is placed in the regular action context. This means it is not propagated to nested actions called via manage_relationship or other nested action invocations.
If you need AshOban context to propagate to nested actions (e.g. so that policy bypasses work in related actions), use the shared_context option. This places the specified keys into Ash's shared context, which is automatically propagated to all nested actions.
# Recommended: share only the job
shared_context [:job]
# Share all AshOban context keys (ash_oban? and job)
shared_context :allshared_context can be set at three levels, with each inheriting from the next if not specified:
- Per trigger or scheduled action — set
shared_contextdirectly on the trigger/schedule - Per resource — set
shared_contextin theobansection of the resource DSL - Application config — set
config :ash_oban, shared_context: [:job]in your app config
This makes it easy to configure shared context globally:
# in config.exs
config :ash_oban, shared_context: [:job]Persisting the actor along with a job
Create a module that is responsible for translating the current user to a value that will be JSON encoded, and for turning that encoded value back into an actor.
defmodule MyApp.AshObanActorPersister do
use AshOban.ActorPersister
def store(%MyApp.User{id: id}), do: %{"type" => "user", "id" => id}
def lookup(%{"type" => "user", "id" => id}), do: MyApp.Accounts.get_user_by_id(id)
# This allows you to set a default actor
# in cases where no actor was present
# when scheduling.
def lookup(nil), do: {:ok, nil}
endThen, configure this in application config.
config :ash_oban, :actor_persister, MyApp.AshObanActorPersisterThis global configuration will affect all oban triggers. You can also configure an actor persister on individual triggers and scheduled actions, i.e
trigger :name do
...
actor_persister MyApp.AshObanActorPersister
endOr you can use :none to override the globally configured actor persister
trigger :name do
...
actor_persister :none
endUsing a default actor without a persister
If your trigger or scheduled action should always run as a fixed system actor
(for example, on a cron schedule that has no real user behind it), you can set
a default_actor directly in the DSL. No actor persister is required.
trigger :nightly_cleanup do
action :cleanup
default_actor %MyApp.SystemActor{id: "system"}
end
scheduled_actions do
schedule :daily_report, "0 0 * * *" do
action :generate_report
default_actor %MyApp.SystemActor{id: "system"}
end
endThe default_actor is a literal value (a map or struct), evaluated at the
resource's compile time. If you use a struct, make sure its module is compiled
before the resource that references it. It is only used when no actor is
supplied via job args. The precedence is:
- Actor supplied at schedule time (via
AshOban.schedule/3,AshOban.run_trigger/3, or a changeset context) and round-tripped through the configuredactor_persister - The
default_actorconfigured on the trigger / scheduled action nil
This is useful for system-actor flows where the actor is constant and there is
nothing to serialize. If you need a fresh database record on each run, configure
an actor_persister and use its lookup(nil) callback instead.
Considerations
There are a few things that are important to keep in mind:
The actor could be deleted or otherwise unavailable when you look it up. You very likely want your
lookup/1to return an error in that scenario.The actor can have changed between when the job was scheduled and when the trigger is executing. It can even change across retries. If you are trying to authorize access for a given trigger's update action to a given actor, keep in mind that just because the trigger is running for a given action, does not mean that the conditions that allowed them to originally schedule that action are still true.