Date: 2025-02-25
Status
Accepted
Context
Our ebook conversion system deals with complex workflows involving multiple external tools, file operations, and conversion processes. Errors can occur at various stages, and our current error handling approach focuses primarily on reporting errors rather than recovering from them.
As identified in ADR-0009 (Implement validation strategy), we need to enhance our system with:
- Graceful degradation where possible
- Clear recovery instructions
- Automatic fixes for common issues
- Rollback capabilities for failed operations
- State preservation during errors
The replatforming to Elixir provides an opportunity to implement more robust error recovery mechanisms, but we need to establish patterns that can be applied to our current Ruby codebase and later migrated to Elixir.
Decision
We will implement a comprehensive error recovery strategy with the following components:
1. Recovery Strategy Pattern
We will create a Recovery Strategy pattern that defines how to recover from specific error types:
module ErrorRecovery
class Strategy
def can_recover?(error)
# Determine if this strategy can handle the given error
end
def recover(error, context)
# Implement recovery logic
# Return {:ok, result} or {:error, new_error}
end
def recovery_instructions(error)
# Return human-readable recovery instructions
end
end
end
2. Retry Mechanism
For transient errors, especially those related to external tools or network operations:
module ErrorRecovery
def self.with_retry(max_attempts: 3, backoff: :exponential, &block)
attempt = 0
begin
attempt += 1
yield
rescue => error
if attempt < max_attempts && retryable?(error)
sleep calculate_backoff(attempt, backoff)
retry
else
raise error
end
end
end
end
3. Circuit Breaker Pattern
For protecting the system from cascading failures when external dependencies fail:
module ErrorRecovery
class CircuitBreaker
def initialize(failure_threshold: 5, reset_timeout: 30)
# Initialize circuit breaker state
end
def execute(&block)
# Check circuit state and execute or fail fast
end
end
end
4. Automatic Recovery for Common Issues
Implement automatic fixes for common issues:
module ErrorRecovery
class AutoFix
def self.register(error_class, &fix)
# Register an automatic fix for an error type
end
def self.attempt_fix(error, context)
# Try to automatically fix the error
end
end
end
5. Transaction-like Operations
For multi-step operations that need to be rolled back on failure:
module ErrorRecovery
def self.with_rollback(context, &block)
snapshot = take_snapshot(context)
begin
result = yield
{:ok, result}
rescue => error
restore_snapshot(context, snapshot)
{:error, error}
end
end
end
Implementation Strategy
-
Phased Implementation:
- Phase 1: Implement core recovery patterns
- Phase 2: Add retry mechanisms for external tools
- Phase 3: Implement circuit breakers for critical dependencies
- Phase 4: Add automatic fixes for common errors
- Phase 5: Implement transaction-like operations
-
Integration with Existing Code:
- Wrap critical operations with recovery mechanisms
- Enhance error messages with recovery instructions
- Add context information to errors for better recovery
- Implement state preservation for long-running operations
-
Elixir Migration Path:
- Map Ruby recovery patterns to Elixir/OTP equivalents
- Use Elixir's supervision trees for more robust recovery
- Leverage GenServer restart strategies
- Implement process isolation for better fault tolerance
Consequences
Positive
- Improved reliability: The system can recover from many errors without manual intervention
- Better user experience: Clear recovery instructions when automatic recovery isn't possible
- Reduced operational overhead: Fewer support requests for common issues
- System resilience: Protection against cascading failures
- State preservation: Less work lost when errors occur
- Clearer error handling: Standardized approach to error recovery
- Migration readiness: Patterns that map well to Elixir/OTP concepts
Negative
- Increased complexity: More sophisticated error handling logic
- Testing challenges: Recovery scenarios need comprehensive testing
- Performance overhead: Some recovery mechanisms add latency
- Development overhead: Implementing recovery logic takes time
- Potential masking of issues: Automatic recovery might hide underlying problems
Mitigation Strategies
- Comprehensive logging: Log all recovery attempts, even successful ones
- Telemetry: Track recovery statistics to identify recurring issues
- Circuit breaker monitoring: Alert on frequently tripped circuit breakers
- Recovery testing: Explicitly test recovery scenarios
- Documentation: Clear documentation of recovery mechanisms
- Timeout controls: Ensure recovery attempts don't block indefinitely
- Fallback degradation: Define acceptable degraded functionality
References
- Michael T. Nygard, "Release It!"
- Martin Fowler, "Circuit Breaker"
- Fred Hebert, "Erlang in Anger"
- Joe Armstrong, "Making reliable distributed systems in the presence of software errors"
- Elixir documentation on error handling and supervision