Ash.Reactor
is an extension for Reactor
which adds explicit support for interacting with resources via their defined actions.
See Getting started with Reactor for more information about Reactor.
You can either add the Ash.Reactor
extension to your existing reactors eg:
defmodule MyExistingReactor do
use Reactor, extensions: [Ash.Reactor]
end
or for your convenience you can use use Ash.Reactor
which expands to exactly the same as above.
An example is worth 1000 words of prose:
defmodule ExampleReactor do
use Ash.Reactor
ash do
default_domain ExampleDomain
end
input :customer_name
input :customer_email
input :plan_name
input :payment_nonce
create :create_customer, Customer do
inputs %{name: input(:customer_name), email: input(:customer_email)}
end
read_one :get_plan, Plan, :get_plan_by_name do
inputs %{name: input(:plan_name)}
fail_on_not_found? true
end
action :take_payment, PaymentProvider do
inputs %{
nonce: input(:payment_nonce),
amount: result(:get_plan, [:price])
}
end
create :subscription, Subscription do
inputs %{
plan_id: result(:get_plan, [:id]),
payment_provider_id: result(:take_payment, :id)
}
end
end
For each action type there is a corresponding step DSL, which needs a name (used to refer to the result of the step by other steps), a resource and optional action name (defaults to the primary action if one is not provided).
Actions have several common options and some specific to their particular type. See the DSL documentation for details.
Ash actions take a map of input parameters which are usually a combination of
resource attributes and action arguments. You can provide these values as a
single map using the inputs
DSL entity with a map or keyword list which refers to Reactor inputs, results and hard-coded values via Reactor's predefined template functions.
For action types that act on a specific resource (ie update
and destroy
) you can provide the value using the initial
DSL option.
input :blog_title
input :blog_body
input :author_email
read :get_author, MyBlog.Author, :get_author_by_email do
inputs %{email: input(:author_email)}
end
create :create_post, MyBlog.Post, :create do
inputs %{
title: input(:blog, [:title]),
body: input(:blog, [:body]),
author_id: result(:get_author, [:email])
}
end
update :author_post_count, MyBlog.Author, :update_post_count do
wait_for :create_post
initial result(:get_author)
end
return :create_post
Reactor is a saga executor, which means that when failure occurs it tries to
clean up any intermediate state left behind. By default the create
, update
and destroy
steps do not specify any behaviour for what to do when there is a
failure downstream in the reactor. This can be changed by providing both an
undo_action
and changing the step's undo
option to either
:outside_transaction
or :always
depending on your resource and datalayer
semantics.
:never
- this is the default, and means that the reactor will never try and undo the action's work. This is the most performant option, as it means that the reactor doesn't need to store as many intermediate values.:outside_transaction
- this option allows the step to decide at runtime whether it should support undo based on whether the action is being run within a transaction. If it is, then no undo is required because the transaction will rollback.:always
- this forces the step to always undo it's work on failure.
The behaviour of the undo_action
is action specific:
- For
create
actions, theundo_action
should be the name of adestroy
action with no specific requirements. - For
update
actions, theundo_action
should also be anupdate
action which takes achangeset
argument, which will contain theAsh.Changeset
which was used to execute the original update. - For
destroy
actions, theundo_action
should be the name of acreate
action which takes arecord
argument, which will contain the resource record which was used destroyed.
You can use the transaction
step type to wrap a group of steps inside a data layer transaction, however the following caveats apply:
- All steps inside a transaction must happen in the same process, so the steps inside the transaction will only ever be executed synchronously.
- Notifications will be sent only when the transaction is committed.
Because a reactor has transaction-like semantics notifications are automatically batched and only sent upon successful completion of the reactor.
Ash's generic actions now support providing a Reactor module directly as their run
option.
Notes:
- Every Reactor input must have a corresponding action argument.
- Ash's action context is passed in as the Reactor's context (including things like actor, tenant, etc).
- Reactor runtime options can be set by setting
run {MyReactor, opts}
instead of justrun MyReactor
. - If you set the
transaction?
action DSL option to true then the Reactor will be run synchronously - regardless of the value of theasync?
runtime option.
action :run_reactor, :struct do
constraints instance_of: MyBlog.Post
argument :blog_title, :string, allow_nil?: false
argument :blog_body, :string, allow_nil?: false
argument :author_email, :ci_string, allow_nil?: false
run MyBlog.CreatePostReactor
end