- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
11 KiB
11 KiB
Error Boundaries
Error Boundaries provide graceful degradation and prevent cascading failures in the application. They act as a safety net that catches errors and provides fallback functionality instead of letting the entire system fail.
Overview
The Error Boundary system implements multiple patterns for resilient error handling:
- Fallback Mechanisms - Provide alternative functionality when operations fail
- Retry Strategies - Automatically retry failed operations with configurable strategies
- Circuit Breaker Pattern - Prevent repeated calls to failing services
- Bulk Operations - Handle partial failures in batch processing
- Timeout Protection - Prevent long-running operations from blocking the system
Basic Usage
Simple Error Boundary
use App\Framework\ErrorBoundaries\ErrorBoundary;
use App\Framework\ErrorBoundaries\BoundaryConfig;
$boundary = new ErrorBoundary('user_service', BoundaryConfig::externalService());
$result = $boundary->execute(
operation: fn() => $userService->getUser($id),
fallback: fn() => $this->getCachedUser($id)
);
Factory Pattern
use App\Framework\ErrorBoundaries\ErrorBoundaryFactory;
$factory = $container->get(ErrorBoundaryFactory::class);
// Create boundary for different contexts
$dbBoundary = $factory->createForDatabase('user_queries');
$apiBoundary = $factory->createForExternalService('payment_api');
$uiBoundary = $factory->createForUI('user_dashboard');
Configuration
Predefined Configurations
// Critical operations - maximum resilience
BoundaryConfig::critical()
// External services - network-aware retries
BoundaryConfig::externalService()
// Database operations - transaction-safe retries
BoundaryConfig::database()
// UI components - fast failure for user experience
BoundaryConfig::ui()
// Background jobs - long retry cycles
BoundaryConfig::backgroundJob()
// Development - permissive for debugging
BoundaryConfig::development()
// Fail fast - no retries
BoundaryConfig::failFast()
Custom Configuration
$config = new BoundaryConfig(
maxRetries: 3,
retryStrategy: RetryStrategy::EXPONENTIAL_JITTER,
baseDelay: Duration::fromMilliseconds(100),
maxDelay: Duration::fromSeconds(5),
circuitBreakerEnabled: true,
circuitBreakerThreshold: 5,
circuitBreakerTimeout: Duration::fromMinutes(1),
maxBulkErrorRate: 0.3,
enableMetrics: true,
enableTracing: false
);
Execution Strategies
Standard Execution with Fallback
$result = $boundary->execute(
operation: fn() => $service->riskyOperation(),
fallback: fn() => $service->safeAlternative()
);
Optional Execution (Returns null on failure)
$result = $boundary->executeOptional(
operation: fn() => $service->optionalOperation(),
fallback: fn() => $service->defaultValue() // Optional fallback
);
Default Value on Failure
$result = $boundary->executeWithDefault(
operation: fn() => $service->getValue(),
defaultValue: 'default_value'
);
Result Wrapper
$result = $boundary->executeForResult(
operation: fn() => $service->operation()
);
if ($result->isSuccess()) {
$value = $result->getValue();
} else {
$error = $result->getError();
}
Retry Strategies
Fixed Delay
RetryStrategy::FIXED // Same delay between retries
Linear Backoff
RetryStrategy::LINEAR // Linearly increasing delay
Exponential Backoff
RetryStrategy::EXPONENTIAL // Exponentially increasing delay
Exponential with Jitter
RetryStrategy::EXPONENTIAL_JITTER // Exponential + random jitter
Circuit Breaker Pattern
$config = new BoundaryConfig(
circuitBreakerEnabled: true,
circuitBreakerThreshold: 5, // Open after 5 failures
circuitBreakerTimeout: Duration::fromMinutes(2) // Try again after 2 minutes
);
$result = $boundary->executeWithCircuitBreaker(
operation: fn() => $externalService->call(),
fallback: fn() => $this->getCachedResponse()
);
Bulk Operations
Handle batch processing with partial failure tolerance:
$items = [1, 2, 3, 4, 5];
$result = $boundary->executeBulk($items, function($item) {
if ($item % 2 === 0) {
throw new Exception("Even numbers fail");
}
return $item * 2;
});
echo "Processed: {$result->getProcessedCount()}/{$result->getTotalCount()}\n";
echo "Success rate: {$result->getSuccessRate()}%\n";
foreach ($result->getResults() as $key => $value) {
echo "Item {$key}: {$value}\n";
}
foreach ($result->getErrors() as $key => $error) {
echo "Error {$key}: {$error->getMessage()}\n";
}
Timeout Protection
$result = $boundary->executeWithTimeout(
operation: fn() => $longRunningService->process(),
fallback: fn() => 'Operation timed out',
timeoutSeconds: 30
);
Parallel Operations
Execute multiple operations with individual boundaries:
$operations = [
'user_data' => fn() => $userService->getData(),
'preferences' => fn() => $prefsService->getPreferences(),
'notifications' => fn() => $notificationService->getCount()
];
$results = $boundary->executeParallel($operations);
foreach ($results as $name => $result) {
if ($result->isSuccess()) {
$data[$name] = $result->getValue();
} else {
$data[$name] = null; // Or default value
}
}
HTTP Middleware Integration
Automatic error boundary protection for HTTP requests:
// In middleware registration
$app->addMiddleware(ErrorBoundaryMiddleware::class);
The middleware automatically:
- Creates boundaries based on route patterns
- Provides JSON fallback responses for API routes
- Provides HTML error pages for web routes
- Logs failures for monitoring
Environment Configuration
Configure boundaries via environment variables:
# Global settings
ERROR_BOUNDARY_ENABLED=true
# Route-specific configuration
ERROR_BOUNDARY_ROUTE_API_USER_MAX_RETRIES=5
ERROR_BOUNDARY_ROUTE_API_USER_CIRCUIT_BREAKER_ENABLED=true
ERROR_BOUNDARY_ROUTE_API_USER_BASE_DELAY_MS=200
Console Commands
Test Error Boundaries
# Test basic functionality
php console.php boundary:test basic
# Test retry strategies
php console.php boundary:test retry
# Test circuit breaker
php console.php boundary:test circuit
# Test bulk operations
php console.php boundary:test bulk
Monitor Circuit Breakers
# Show circuit breaker statistics
php console.php boundary:stats
# Reset specific circuit breaker
php console.php boundary:reset user_service
# Reset all circuit breakers
php console.php boundary:reset
Best Practices
1. Choose Appropriate Configurations
// API endpoints - use external service config
$apiBoundary = $factory->createForExternalService('payment_api');
// Database queries - use database config
$dbBoundary = $factory->createForDatabase('user_queries');
// UI components - use UI config for fast failures
$uiBoundary = $factory->createForUI('dashboard_widget');
2. Meaningful Fallbacks
// Good - provides useful fallback
$boundary->execute(
operation: fn() => $service->getLiveData(),
fallback: fn() => $service->getCachedData()
);
// Avoid - fallback provides no value
$boundary->execute(
operation: fn() => $service->getData(),
fallback: fn() => null
);
3. Monitor Circuit Breakers
// Set up alerting for circuit breaker state changes
$boundary->executeWithCircuitBreaker(
operation: fn() => $service->call(),
fallback: function() {
$this->logger->warning('Circuit breaker activated for service');
return $this->getFallbackData();
}
);
4. Handle Bulk Operations Appropriately
$result = $boundary->executeBulk($items, $processor);
// Check if too many failures occurred
if ($result->getErrorRate() > 50) {
$this->logger->error('High error rate in bulk operation');
// Consider stopping or alerting
}
Integration Examples
With Repositories
class UserRepository
{
public function __construct(
private ErrorBoundaryFactory $boundaryFactory,
private DatabaseConnection $db
) {}
public function findById(int $id): ?User
{
$boundary = $this->boundaryFactory->createForDatabase('user_find');
return $boundary->executeOptional(
operation: fn() => $this->db->query('SELECT * FROM users WHERE id = ?', [$id]),
fallback: fn() => $this->getCachedUser($id)
);
}
}
With External APIs
class PaymentService
{
public function processPayment(Payment $payment): PaymentResult
{
$boundary = $this->boundaryFactory->createForExternalService('payment_gateway');
return $boundary->execute(
operation: fn() => $this->gateway->process($payment),
fallback: fn() => $this->queueForLaterProcessing($payment)
);
}
}
With Background Jobs
class EmailJob
{
public function handle(): void
{
$boundary = $this->boundaryFactory->createForBackgroundJob('email_sending');
$boundary->executeBulk($this->emails, function($email) {
$this->mailer->send($email);
});
}
}
Error Handling
Error boundaries can throw specific exceptions:
BoundaryFailedException- When both operation and fallback failBoundaryTimeoutException- When operations exceed timeout limits
try {
$result = $boundary->execute($operation, $fallback);
} catch (BoundaryFailedException $e) {
$this->logger->error('Boundary failed completely', [
'boundary' => $e->getBoundaryName(),
'original_error' => $e->getOriginalException()?->getMessage(),
'fallback_error' => $e->getFallbackException()?->getMessage(),
]);
} catch (BoundaryTimeoutException $e) {
$this->logger->warning('Operation timed out', [
'boundary' => $e->getBoundaryName(),
'execution_time' => $e->getExecutionTime(),
'timeout_limit' => $e->getTimeoutLimit(),
]);
}
Performance Considerations
- Circuit Breakers - Use file-based storage for simplicity, consider Redis for high-traffic applications
- Retry Delays - Use jitter to avoid thundering herd problems
- Bulk Operations - Set appropriate error rate thresholds to prevent resource exhaustion
- Timeouts - PHP's synchronous nature limits true timeout implementation
Security Considerations
- Information Disclosure - Ensure fallbacks don't leak sensitive information
- Resource Exhaustion - Configure appropriate timeouts and retry limits
- Circuit Breaker State - Protect circuit breaker state files from unauthorized access