Packages
reactor
0.16.0
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
documentation/how-to/debugging-workflows.md
# How to Debug Reactor Workflows
## Problem
Your reactor isn't behaving as expected and you need effective techniques to troubleshoot and debug workflow execution, identify bottlenecks, and understand what's happening during execution.
## Solution Overview
This guide shows you debugging techniques, tools, and patterns for identifying and fixing issues in your reactors using Reactor's built-in debugging features, telemetry, visualization tools, and error analysis.
## Prerequisites
- Understanding of Reactor basics
- Experience building reactors
- Basic knowledge of Elixir debugging techniques
## Debugging Strategies
### 1. Using Debug Steps
The simplest way to understand what's happening in your reactor is to add debug steps that log intermediate values.
#### Basic Debug Step
```elixir
defmodule UserProcessingReactor do
use Reactor
input :user_data
step :validate_user, MyApp.Steps.ValidateUser do
argument :user_data, input(:user_data)
end
debug :check_validation do
argument :user, result(:validate_user)
level :info
end
step :create_user, MyApp.Steps.CreateUser do
argument :user, result(:validate_user)
end
debug :check_creation do
argument :result, result(:create_user)
end
end
```
#### Debug Output
Debug steps log comprehensive information about arguments, context, and options:
```
[info] # Debug information for step `:check_validation`.
## Arguments
%{
user: %{
email: "user@example.com",
name: "John Doe",
validated: true
}
}
## Context
%{
concurrency_key: #Reference<0.123.456.789>,
current_step: :check_validation
}
## Options
[]
```
### 2. Synchronous Execution for Debugging
When debugging, disable async execution to get predictable, deterministic behavior:
```elixir
defmodule DebuggingTest do
test "debug complex workflow" do
inputs = %{user_data: sample_data()}
result = Reactor.run(UserProcessingReactor, inputs, async?: false)
case result do
{:ok, user} ->
IO.puts("Success: #{inspect(user)}")
{:error, errors} ->
IO.puts("Failed: #{inspect(errors)}")
end
end
end
```
### 3. Visual Workflow Debugging
Generate visual diagrams to understand reactor structure and flow:
#### Creating Mermaid Diagrams
Use the built-in Mix task to generate visual diagrams:
```bash
# Generate basic diagram
mix reactor.mermaid UserProcessingReactor
# Include descriptions and expand sub-reactors
mix reactor.mermaid UserProcessingReactor --describe --expand
# Save to specific file
mix reactor.mermaid UserProcessingReactor --output debug_flow.mmd
# Display diagram for copy-pasting into Mermaid Live
mix reactor.mermaid UserProcessingReactor --format copy
# Generate direct Mermaid Live Editor URL
mix reactor.mermaid UserProcessingReactor --format url
```
Or generate diagrams programmatically:
```elixir
defmodule DebugHelpers do
def visualize_reactor(reactor_module, filename \\ "reactor_debug.mmd") do
{:ok, diagram} = Reactor.Mermaid.to_mermaid(reactor_module,
describe?: true,
expand?: true
)
File.write!(filename, diagram)
IO.puts("Diagram saved to #{filename}")
IO.puts("View at: https://mermaid.live/edit")
end
end
DebugHelpers.visualize_reactor(UserProcessingReactor)
```
#### Understanding the Diagram
The generated diagram shows:
- Input dependencies
- Step execution order
- Data flow between steps
- Conditional branches and loops
### 4. Telemetry-Based Debugging
Set up telemetry handlers to monitor reactor execution in real-time:
#### Setting Up Telemetry
```elixir
defmodule MyApp.ReactorTelemetry do
def setup_debugging() do
events = [
[:reactor, :run, :start],
[:reactor, :run, :stop],
[:reactor, :step, :run, :start],
[:reactor, :step, :run, :stop]
]
:telemetry.attach_many(
"reactor-debug",
events,
&handle_event/4,
nil
)
end
def handle_event([:reactor, :run, :start], _measurements, metadata, _config) do
IO.puts("π Starting reactor: #{metadata.reactor.id}")
end
def handle_event([:reactor, :run, :stop], measurements, metadata, _config) do
duration_ms = measurements.duration / 1_000_000
case metadata.outcome do
:ok ->
IO.puts("β
Reactor completed in #{duration_ms}ms")
:error ->
IO.puts("β Reactor failed in #{duration_ms}ms")
:halt ->
IO.puts("βΈοΈ Reactor halted in #{duration_ms}ms")
end
end
def handle_event([:reactor, :step, :run, :start], _measurements, metadata, _config) do
IO.puts(" π Starting step: #{metadata.step.name}")
end
def handle_event([:reactor, :step, :run, :stop], measurements, metadata, _config) do
duration_ms = measurements.duration / 1_000_000
case metadata.outcome do
:ok ->
IO.puts(" β
Step #{metadata.step.name} completed in #{duration_ms}ms")
:error ->
IO.puts(" β Step #{metadata.step.name} failed in #{duration_ms}ms")
:retry ->
IO.puts(" π Step #{metadata.step.name} retrying after #{duration_ms}ms")
end
end
end
```
#### Using Telemetry in Your Reactor
```elixir
defmodule MyReactor do
use Reactor
middlewares do
middleware Reactor.Middleware.Telemetry
end
# ... steps
end
# Set up debugging and run
MyApp.ReactorTelemetry.setup_debugging()
Reactor.run(MyReactor, inputs)
```
### 5. Error Analysis and Debugging
Understand and debug errors using Reactor's structured error system:
#### Analyzing Reactor Errors
```elixir
defmodule ErrorAnalyzer do
def debug_error(error) do
IO.puts("π Analyzing reactor error...")
case error do
%Reactor.Error.Invalid.RunStepError{} = step_error ->
IO.puts("β Step execution failed:")
IO.puts(" Step: #{step_error.step.name}")
IO.puts(" Error: #{inspect(step_error.error)}")
debug_step_context(step_error.step)
%Reactor.Error.Invalid.MissingArgumentError{} = arg_error ->
IO.puts("β Missing argument:")
IO.puts(" Argument: #{arg_error.argument.name}")
IO.puts(" Step: #{arg_error.step.name}")
debug_dependency_chain(arg_error.step, arg_error.argument)
%Reactor.Error.Validation.MissingReturnError{} ->
IO.puts("β No return value specified for reactor")
IO.puts("π‘ Add a return statement to your reactor")
errors when is_list(errors) ->
IO.puts("β Multiple errors occurred:")
Enum.with_index(errors, 1)
|> Enum.each(fn {err, idx} ->
IO.puts(" #{idx}. #{inspect(err)}")
end)
_ ->
IO.puts("β Unknown error: #{inspect(error)}")
end
end
defp debug_step_context(step) do
IO.puts(" Arguments:")
Enum.each(step.arguments, fn arg ->
IO.puts(" - #{arg.name}: #{inspect(arg.source)}")
end)
end
defp debug_dependency_chain(step, argument) do
IO.puts("π‘ Check if the source step completed successfully:")
case argument.source do
%Reactor.Template.Result{name: source_step} ->
IO.puts(" Source step: #{source_step}")
IO.puts(" Add a debug step after #{source_step} to verify its output")
%Reactor.Template.Input{name: input_name} ->
IO.puts(" Missing input: #{input_name}")
IO.puts(" Ensure this input is provided when running the reactor")
_ ->
IO.puts(" Source: #{inspect(argument.source)}")
end
end
end
# Usage
case Reactor.run(MyReactor, inputs) do
{:ok, result} ->
IO.puts("Success: #{inspect(result)}")
{:error, errors} ->
Enum.each(List.wrap(errors), &ErrorAnalyzer.debug_error/1)
end
```
### 6. Performance Debugging
Identify performance bottlenecks in your workflows:
#### Performance Monitoring
```elixir
defmodule PerformanceMonitor do
def setup_performance_monitoring(slow_threshold_ms \\ 100) do
:telemetry.attach(
"reactor-performance",
[:reactor, :step, :run, :stop],
&monitor_step_performance/4,
%{threshold: slow_threshold_ms * 1_000_000} # Convert to nanoseconds
)
end
def monitor_step_performance(_event, measurements, metadata, config) do
if measurements.duration > config.threshold do
duration_ms = measurements.duration / 1_000_000
IO.puts("π Slow step detected:")
IO.puts(" Step: #{metadata.step.name}")
IO.puts(" Duration: #{duration_ms}ms")
IO.puts(" Module: #{metadata.step.impl}")
end
end
end
PerformanceMonitor.setup_performance_monitoring(50) # 50ms threshold
```
#### Concurrency Debugging
```elixir
defmodule ConcurrencyDebugger do
def debug_concurrency(reactor_module, inputs) do
# Monitor async step spawning
:telemetry.attach(
"concurrency-debug",
[:reactor, :step, :process, :start],
fn _event, _measurements, metadata, _config ->
IO.puts("π Spawned async step: #{metadata.step.name} (PID: #{inspect(metadata.pid)})")
end,
nil
)
# Run with limited concurrency for debugging
Reactor.run(reactor_module, inputs, max_concurrency: 2)
end
end
```
### 7. Interactive Debugging
Use IEx for interactive debugging sessions:
#### Adding Breakpoints
```elixir
defmodule MyApp.Steps.DebugStep do
use Reactor.Step
def run(arguments, context, _options) do
IO.puts("π Interactive debugging point")
IO.puts("Arguments: #{inspect(arguments)}")
IO.puts("Context: #{inspect(context)}")
# Start interactive session
require IEx; IEx.pry()
{:ok, arguments}
end
end
# Use in reactor
step :debug_point, MyApp.Steps.DebugStep do
argument :data, result(:previous_step)
end
```
#### Runtime Inspection
```elixir
defmodule ReactorInspector do
def inspect_reactor_state(reactor_module) do
{:ok, reactor_struct} = Reactor.Info.to_struct(reactor_module)
IO.puts("π Reactor Analysis:")
IO.puts(" Inputs: #{length(reactor_struct.inputs)}")
IO.puts(" Steps: #{length(reactor_struct.steps)}")
IO.puts("\nπ Step Details:")
Enum.each(reactor_struct.steps, fn step ->
IO.puts(" #{step.name}:")
IO.puts(" Module: #{step.impl}")
IO.puts(" Async: #{step.async?}")
IO.puts(" Arguments: #{length(step.arguments)}")
end)
if reactor_struct.plan do
IO.puts("\nπΊοΈ Execution Plan:")
IO.inspect(reactor_struct.plan)
end
end
end
```
## Debugging Workflow
1. **Start with synchronous execution** (`async?: false`)
2. **Add debug steps** at key points in your workflow
3. **Generate visual diagrams** to understand data flow
4. **Set up telemetry monitoring** for performance insights
5. **Use error analysis** to understand failure modes
6. **Add interactive breakpoints** for complex issues
## Common Debugging Scenarios
### Step Not Executing
```elixir
# Check if dependencies are met
debug :before_problem_step do
argument :data, result(:previous_step)
end
step :problem_step do
argument :data, result(:previous_step)
# ...
end
```
### Unexpected Results
```elixir
# Compare expected vs actual
debug :validate_result do
argument :result, result(:calculation_step)
end
step :verify_result do
argument :result, result(:calculation_step)
run fn %{result: result}, _context ->
expected = 42
if result == expected do
{:ok, result}
else
{:error, "Expected #{expected}, got #{result}"}
end
end
end
```
## Related Guides
- [Testing Strategies](testing-strategies.md) - Test your debugging assumptions
- [Performance Optimization](performance-optimization.md) - Optimize identified bottlenecks
- [Error Handling Tutorial](../tutorials/02-error-handling.md) - Handle errors gracefully
This comprehensive debugging approach helps you quickly identify and resolve issues in your reactor workflows.