Date: 2025-03-01
Status
Accepted
Context
As part of our migration from Ruby to Elixir, we need to design a robust supervision strategy that leverages OTP's fault tolerance capabilities. Our current Ruby implementation lacks structured error recovery and process isolation, which has led to several issues:
- Cascading failures: Errors in one component can affect the entire system
- Manual recovery: Operators must manually restart failed processes
- Limited visibility: Difficult to track component health and state
- Inconsistent error handling: Different approaches across components
- Resource leaks: Failed operations may leave resources in inconsistent states
The Elixir/OTP platform provides built-in supervision capabilities that can address these issues through structured process management, isolation, and automatic recovery.
Decision
We will implement a comprehensive supervision strategy with the following structure:
1. Top-Level Application Supervisor
BookProcessing.Supervisor (one_for_one)
│
├── BookProcessing.Configuration.Supervisor (one_for_one)
├── BookProcessing.Conversion.Supervisor (one_for_one)
├── BookProcessing.Validation.Supervisor (rest_for_one)
├── BookProcessing.Build.Supervisor (one_for_all)
├── BookProcessing.ErrorHandling.Supervisor (one_for_one)
└── BookProcessing.Telemetry.Supervisor (one_for_one)
2. Context-Specific Supervisors
Each bounded context will have its own supervisor with an appropriate strategy:
Configuration Supervisor (one_for_one)
- ConfigurationManager: Manages application configuration
- PathResolver: Handles path resolution and validation
- EnvironmentDetector: Detects and manages environment settings
Conversion Supervisor (one_for_one)
- LaTeXConverter: Handles LaTeX conversions
- DocxConverter: Handles DOCX conversions
- PandocConverter: Manages Pandoc operations
- FormatRegistry: Maintains registry of available formats
Validation Supervisor (rest_for_one)
- ValidationCoordinator: Coordinates validation processes
- FileValidator: Validates file integrity and format
- EpubValidator: EPUB-specific validation
- PdfValidator: PDF-specific validation
- MetadataValidator: Validates metadata completeness and correctness
Build Supervisor (one_for_all)
- BuildCoordinator: Orchestrates the build process
- FormatBuilder: Builds specific formats
- OutputManager: Manages output directories and artifacts
ErrorHandling Supervisor (one_for_one)
- ErrorLogger: Centralized error logging
- ErrorReporter: Reports errors to monitoring systems
- RecoveryCoordinator: Coordinates error recovery strategies
Telemetry Supervisor (one_for_one)
- MetricsCollector: Collects system metrics
- MetricsReporter: Reports metrics to monitoring systems
- HealthMonitor: Monitors system health
3. Supervision Strategies
We will use different supervision strategies based on the nature of each component:
- one_for_one: When components are independent (default)
- one_for_all: When components are tightly coupled and must be restarted together
- rest_for_one: When components depend on previous components in a chain
- simple_one_for_one: For dynamically created workers of the same type
4. Process Types
We will use appropriate OTP behaviors based on component requirements:
- GenServer: For stateful components that need to maintain state
- Task: For short-lived, concurrent operations
- Agent: For simple state storage
- GenStage: For data processing pipelines
- DynamicSupervisor: For dynamically created child processes
5. Restart Strategies
We will configure appropriate restart strategies for different components:
- permanent: Always restart (default for critical services)
- temporary: Never restart (for one-off tasks)
- transient: Restart only on abnormal termination (for most workers)
6. Circuit Breakers
We will implement circuit breakers for external dependencies:
defmodule BookProcessing.CircuitBreaker do
use GenServer
# Circuit breaker implementation
# ...
end
Implementation Example
defmodule BookProcessing.Application do
use Application
def start(_type, _args) do
children = [
{BookProcessing.Configuration.Supervisor, []},
{BookProcessing.Conversion.Supervisor, []},
{BookProcessing.Validation.Supervisor, []},
{BookProcessing.Build.Supervisor, []},
{BookProcessing.ErrorHandling.Supervisor, []},
{BookProcessing.Telemetry.Supervisor, []}
]
opts = [strategy: :one_for_one, name: BookProcessing.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule BookProcessing.Conversion.Supervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
{BookProcessing.Conversion.LaTeXConverter, []},
{BookProcessing.Conversion.DocxConverter, []},
{BookProcessing.Conversion.PandocConverter, []},
{BookProcessing.Conversion.FormatRegistry, []}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
Consequences
Positive
- Improved fault tolerance: Failures are isolated to specific components
- Automatic recovery: Failed processes are automatically restarted
- Clear process boundaries: Each component has well-defined responsibilities
- Visibility: Process state and health can be monitored
- Resource management: Resources are properly cleaned up on process termination
- Scalability: Components can be distributed across nodes if needed
- Simplified error handling: Let-it-crash philosophy simplifies error handling
Negative
- Increased complexity: More sophisticated architecture to understand
- Learning curve: Team needs to learn OTP concepts
- Process overhead: Each process adds some memory overhead
- Message passing overhead: Communication between processes adds latency
- State management challenges: Need to carefully design stateful components
Mitigation Strategies
- Documentation: Thoroughly document supervision tree and component responsibilities
- Training: Provide team training on OTP concepts and best practices
- Monitoring: Implement comprehensive monitoring of process health
- Testing: Create tests specifically for supervision and recovery scenarios
- Gradual implementation: Start with core components and expand supervision tree incrementally
References
- "Designing for Scalability with Erlang/OTP" by Francesco Cesarini and Steve Vinoski
- "Elixir in Action" by Saša Jurić
- "Erlang and OTP in Action" by Martin Logan, Eric Merritt, and Richard Carlsson
- "Concurrent Data Processing in Elixir" by Svilen Gospodinov
- Erlang documentation on supervision principles
- Elixir documentation on Supervisor and GenServer