Packages
reactor
0.15.4
1.0.2
1.0.1
1.0.0
0.17.0
0.16.0
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
An asynchronous, graph-based execution engine
Current section
Files
Jump to
Current section
Files
usage-rules.md
# Rules for working with Reactor
## Understanding Reactor
Reactor is a dynamic, concurrent, dependency resolving saga orchestrator for Elixir. It provides:
- **Saga orchestration** - Transaction-like semantics across multiple distinct resources with rollback capabilities
- **Dependency resolution** - Automatic calculation of execution order based on step dependencies using a directed acyclic graph (DAG)
- **Concurrent execution** - Runs as many steps as possible concurrently while respecting dependencies
- **Dynamic workflows** - Build workflows at runtime and add steps while the reactor is running
- **Composable DSL** - Declarative approach to defining workflows
Read documentation *before* attempting to use Reactor features. Do not assume prior knowledge of the framework or its conventions.
## Core Concepts
### Reactors
A Reactor is a workflow definition that contains inputs, steps, and their dependencies. Reactors can be defined using the DSL or built programmatically.
### Steps
Steps are the unit of work in a Reactor. Each step:
- Has a unique name
- Can depend on inputs or results from other steps
- Can run synchronously or asynchronously
- Can be compensated (handle errors) or undone (rollback on failure)
- Returns a result that other steps can use
### Arguments
Arguments define dependencies between steps. They specify:
- What data a step needs
- Where that data comes from (inputs or other step results)
- Optional transformations to apply to the data
## Code Structure & Organization
- Define Reactors as modules using the DSL for static workflows
- Use `Reactor.Builder` for dynamic workflow construction
- Create custom steps by implementing the `Reactor.Step` behaviour
- Organize complex workflows into composable sub-reactors
- Use meaningful names for inputs, steps, and arguments
## Basic Reactor DSL
Define a Reactor using the DSL:
```elixir
defmodule MyApp.UserRegistrationReactor do
use Reactor
# Define inputs (like function arguments)
input :email
input :password
input :plan_name
# Define steps with dependencies
step :validate_email do
argument :email, input(:email)
run fn %{email: email}, _ ->
if String.contains?(email, "@") do
{:ok, email}
else
{:error, "Invalid email"}
end
end
end
step :hash_password do
argument :password, input(:password)
run fn %{password: password}, _ ->
{:ok, Bcrypt.hash_pwd_salt(password)}
end
end
step :create_user, MyApp.Steps.CreateUser do
argument :email, result(:validate_email)
argument :password_hash, result(:hash_password)
end
# Specify what to return
return :create_user
end
```
## Step Implementation
### Using Anonymous Functions
For simple steps, use anonymous functions directly in the DSL:
```elixir
step :transform_data do
argument :input, input(:raw_data)
run fn %{input: data}, _ ->
{:ok, String.upcase(data)}
end
end
```
### Using Step Modules
For complex logic, implement the `Reactor.Step` behaviour:
```elixir
defmodule MyApp.Steps.CreateUser do
use Reactor.Step
@impl true
def run(arguments, context, options) do
case create_user(arguments.email, arguments.password_hash) do
{:ok, user} -> {:ok, user}
{:error, reason} -> {:error, reason}
end
end
@impl true
def compensate(reason, arguments, context, options) do
# Handle errors - decide whether to retry or continue with rollback
case reason do
%DBConnection.ConnectionError{} -> :retry
_other -> :ok
end
end
@impl true
def undo(user, arguments, context, options) do
# Rollback successful execution
case delete_user(user) do
:ok -> :ok
{:error, reason} -> {:error, reason}
end
end
defp create_user(email, password_hash) do
# Implementation here
end
defp delete_user(user) do
# Implementation here
end
end
```
## Step Return Values
Steps can return various values to control reactor execution:
- `{:ok, value}` - Success with result value
- `{:ok, value, [step]}` - Success with additional steps to add
- `{:error, reason}` - Failure (triggers compensation/undo)
- `:retry` or `{:retry, reason}` - Retry the step
- `{:halt, reason}` - Pause reactor execution
## Arguments and Dependencies
### Basic Arguments
```elixir
step :example do
# Use input directly
argument :email, input(:email)
# Use result from another step
argument :user, result(:create_user)
# Use a static value
argument :timeout, value(5000)
end
```
### Argument Transformations
Transform argument values before passing to steps:
```elixir
step :example do
# Transform with anonymous function
argument :user_id do
source result(:create_user)
transform &(&1.id)
end
# Extract nested values
argument :birth_year, input(:user_data, [:birth_date, :year])
# Transform input
argument :age do
source input(:birth_year)
transform fn year -> Date.utc_today().year - year end
end
end
```
## Built-in Step Types
### Debug Steps
Log information during execution:
```elixir
debug :log_user do
argument :user, result(:create_user)
argument :message, value("User created successfully")
end
```
### Map Steps
Process collections by applying steps to each element:
```elixir
map :process_users do
source input(:user_list)
batch_size 10
allow_async? true
step :validate_user do
argument :user, element(:process_users)
run fn %{user: user}, _ ->
validate_user(user)
end
end
end
```
### Compose Steps
Embed one reactor inside another:
```elixir
compose :sub_workflow, MyApp.SubReactor do
argument :input_data, result(:prepare_data)
end
```
### Switch Steps
Conditional execution based on predicates:
```elixir
switch :handle_user_type do
on result(:user)
matches? &(&1.type == :premium) do
step :setup_premium_features do
argument :user, result(:user)
# Premium setup logic
end
end
default do
step :setup_basic_features do
argument :user, result(:user)
# Basic setup logic
end
end
end
```
### Group Steps
Execute related steps together with shared setup/teardown:
```elixir
group :user_setup do
before_all &MyApp.setup_database/3
after_all &MyApp.cleanup_database/1
step :create_profile do
# Profile creation logic
end
step :send_welcome_email do
# Email logic
end
end
```
### Around Steps
Wrap step execution with custom logic:
```elixir
around :transaction, &MyApp.with_transaction/4 do
step :create_user do
# User creation in transaction
end
step :create_profile do
# Profile creation in transaction
end
end
```
### Collect Steps
Gather multiple values into a single structure:
```elixir
collect :user_summary do
argument :user, result(:create_user)
argument :profile, result(:create_profile)
argument :settings, result(:create_settings)
transform fn inputs ->
%{
user: inputs.user,
profile: inputs.profile,
settings: inputs.settings
}
end
end
```
## Error Handling and Compensation
### Compensation
Handle step failures and decide how to proceed:
```elixir
defmodule MyApp.Steps.ApiCall do
use Reactor.Step
def run(arguments, context, options) do
case make_api_call(arguments.url) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
end
end
def compensate(reason, arguments, context, options) do
case reason do
# Retry on network errors
%HTTPoison.Error{reason: :timeout} -> :retry
%HTTPoison.Error{reason: :econnrefused} -> :retry
# Continue with error for other failures
_other -> :ok
end
end
end
```
### Undo Operations
Rollback successful operations when later steps fail:
```elixir
defmodule MyApp.Steps.CreateResource do
use Reactor.Step
def run(arguments, context, options) do
{:ok, create_resource(arguments)}
end
def undo(resource, arguments, context, options) do
case delete_resource(resource) do
:ok -> :ok
{:error, :not_found} -> :ok # Already deleted
{:error, reason} -> {:error, reason}
end
end
end
```
## Async vs Sync Execution
### Async Steps (Default)
Steps run asynchronously by default:
```elixir
step :async_operation do
# Runs asynchronously
run fn _, _ -> {:ok, "result"} end
end
```
### Sync Steps
Force synchronous execution:
```elixir
step :sync_operation do
async? false
run fn _, _ -> {:ok, "result"} end
end
```
### Conditional Async
Make async behavior conditional:
```elixir
step :conditional_async do
async? fn options -> options[:force_sync] != true end
run fn _, _ -> {:ok, "result"} end
end
```
## Running Reactors
### Basic Execution
```elixir
# Run with inputs
{:ok, result} = Reactor.run(MyApp.UserRegistrationReactor,
email: "user@example.com",
password: "secret123",
plan_name: "premium"
)
# Run with options
{:ok, result} = Reactor.run(reactor, inputs, context,
async?: false,
max_concurrency: 10
)
```
### Halting and Resuming
```elixir
# Step returns {:halt, reason}
{:halted, reactor_state} = Reactor.run(MyReactor, inputs)
# Resume later
{:ok, result} = Reactor.run(reactor_state, %{}, %{})
```
## Middleware
Add cross-cutting concerns with middleware:
```elixir
defmodule MyApp.LoggingMiddleware do
use Reactor.Middleware
def init(context) do
Logger.info("Reactor starting")
{:ok, context}
end
def complete(result, context) do
Logger.info("Reactor completed successfully")
{:ok, result}
end
def error(errors, context) do
Logger.error("Reactor failed: #{inspect(errors)}")
:ok
end
def event({:run_start, args}, step, context) do
Logger.debug("Step #{step.name} starting with #{inspect(args)}")
end
def event({:run_complete, result}, step, context) do
Logger.debug("Step #{step.name} completed with #{inspect(result)}")
end
end
# Add to reactor
defmodule MyApp.ReactorWithMiddleware do
use Reactor
middlewares do
middleware MyApp.LoggingMiddleware
middleware Reactor.Middleware.Telemetry
end
# Steps...
end
```
## Guards and Conditions
Control step execution with guards:
```elixir
step :conditional_step do
argument :user, result(:create_user)
# Only run if guard passes
guard &(&1.user.active?)
run fn %{user: user}, _ ->
{:ok, "Processing active user: #{user.name}"}
end
end
```
Use where clauses for complex conditions:
```elixir
step :premium_feature do
argument :user, result(:create_user)
where fn %{user: user} ->
user.plan == :premium and user.active?
end
run fn %{user: user}, _ ->
enable_premium_features(user)
end
end
```
## Wait For Dependencies
Explicit dependencies without data flow:
```elixir
step :send_notification do
argument :user, result(:create_user)
# Wait for email verification to complete
wait_for :verify_email
run fn %{user: user}, _ ->
send_welcome_notification(user)
end
end
```
## Best Practices
### Error Handling
- Implement `compensate/4` for retryable errors
- Implement `undo/4` for operations that need rollback
- Use specific error types to guide compensation logic
- Log errors appropriately for debugging
- If available, do work transactionally if possible
- If building an Ash application, use `Ash.Reactor` steps
### Performance
- Use `async? false` sparingly - only when order matters
- Set appropriate `batch_size` for map operations
- Use `strict_ordering? false` in map steps when order doesn't matter
- Consider `max_concurrency` limits for resource-constrained operations
### Code Organization
- Create reusable step modules for common operations
- Use compose steps to break complex workflows into smaller reactors
- Group related steps with shared setup/teardown
- Use meaningful names for all components
### Testing
- Test steps in isolation using the step module directly
- Test reactors with various input combinations
- Test error scenarios and compensation logic
- Use `async? false` in tests for deterministic execution
### Debugging
- Use debug steps to log intermediate values
- Add telemetry middleware for observability
- Use descriptive names and descriptions for steps
- Test with `async? false` to simplify debugging
## Advanced Features
### Context Usage
Pass data through the reactor context:
```elixir
# In a step
def run(arguments, context, options) do
user = context[:current_user]
# Use context data
end
# Run with context
Reactor.run(reactor, inputs, %{current_user: user})
```
### Transform Functions
Apply transformations to inputs and arguments:
```elixir
input :birth_date do
transform &Date.from_iso8601!/1
end
step :calculate_age do
argument :birth_date do
source input(:birth_date)
transform fn date -> Date.diff(Date.utc_today(), date) end
end
end
```
### Retries and Limits
Control retry behavior:
```elixir
step :api_call do
max_retries 3
run fn args, _ ->
# May fail and retry up to 3 times
end
compensate fn reason, _, _, _ ->
case reason do
%HTTPError{status: 503} -> :retry
_ -> :ok
end
end
end
```
### Reactor Composition
Build larger workflows from smaller ones:
```elixir
defmodule MyApp.MainWorkflow do
use Reactor
input :user_data
# Run sub-workflow
compose :user_setup, MyApp.UserSetupReactor do
argument :data, input(:user_data)
end
# Continue with more steps
step :finalize do
argument :setup_result, result(:user_setup)
# Finalization logic
end
end
```