documentation/topics/advanced/reactor.md
Ash.Reactor is an extension for Reactor which adds explicit support for interacting with resources via their defined actions.
See Getting started with Reactor to understand the core Reactor concepts first. Then return to this guide to see how Ash.Reactor adds conveniences for using Reactor from Ash.
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.
Ash's generic actions support providing a Reactor module directly as their run option.
This is the preferred way for you to initiate reactors in your application. These actions could be defined on your existing resources, or you could even have a resource with a single action on it that runs a reactor, and no attributes/data layer etc. for example.
Notes:
run {MyReactor, opts} instead of just run MyReactor.transaction? action DSL option to true then the Reactor will be run synchronously - regardless of the value of the async? runtime option.Resources can just have generic actions {: .info}
Below is a fully valid resource in its entirety. Not all resources need to have state/data layers associated with them
defmodule MyApp.Blog.Actions do
use Ash.Resource
action :create_post, :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 MyApp.Blog.Reactors.CreatePost
end
end
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.
undo option: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.undo_action optionThe behaviour of the undo_action is action specific:
create actions, the undo_action should be the name of a destroy
or update action with no specific requirements.update actions, the undo_action should also be an update action
which takes a changeset argument, which will contain the Ash.Changeset
which was used to execute the original update.destroy actions, the undo_action should be the name of a create
action which takes a record 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:
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
transaction :create_post_transaction, [MyBlog.Post, MyBlog.Author] do
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
end
return :create_post_transaction
Because a reactor has transaction-like semantics notifications are automatically batched and only sent upon successful completion of the reactor.