Files
desk-moloni/modules/desk_moloni/libraries/RetryHandler.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

649 lines
20 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Retry Handler
* Advanced retry logic with exponential backoff, jitter, and circuit breaker pattern
*
* @package DeskMoloni
* @subpackage Libraries
* @category RetryLogic
* @author Descomplicar® - PHP Fullstack Engineer
* @version 1.0.0
*/
namespace DeskMoloni\Libraries;
use DeskMoloni\Libraries\ErrorHandler;
class RetryHandler
{
protected $CI;
protected $model;
protected $error_handler;
// Retry configuration
const DEFAULT_MAX_ATTEMPTS = 5;
const DEFAULT_BASE_DELAY = 1; // seconds
const DEFAULT_MAX_DELAY = 300; // 5 minutes
const DEFAULT_BACKOFF_MULTIPLIER = 2;
const DEFAULT_JITTER_ENABLED = true;
// Circuit breaker configuration
const CIRCUIT_BREAKER_FAILURE_THRESHOLD = 10;
const CIRCUIT_BREAKER_TIMEOUT = 300; // 5 minutes
const CIRCUIT_BREAKER_SUCCESS_THRESHOLD = 3;
// Retry strategies
const STRATEGY_EXPONENTIAL = 'exponential';
const STRATEGY_LINEAR = 'linear';
const STRATEGY_FIXED = 'fixed';
const STRATEGY_FIBONACCI = 'fibonacci';
// Circuit breaker states
const CIRCUIT_CLOSED = 'closed';
const CIRCUIT_OPEN = 'open';
const CIRCUIT_HALF_OPEN = 'half_open';
// Retryable error types
protected $retryable_errors = [
'connection_timeout',
'read_timeout',
'network_error',
'server_error',
'rate_limit',
'temporary_unavailable',
'circuit_breaker_open'
];
// Non-retryable error types
protected $non_retryable_errors = [
'authentication_failed',
'authorization_denied',
'invalid_data',
'resource_not_found',
'bad_request',
'conflict'
];
public function __construct()
{
$this->CI = &get_instance();
$this->CI->load->model('desk_moloni_model');
$this->model = $this->CI->desk_moloni_model;
$this->error_handler = new ErrorHandler();
log_activity('RetryHandler initialized');
}
/**
* Calculate retry delay with exponential backoff
*
* @param int $attempt_number
* @param string $strategy
* @param array $options
* @return int Delay in seconds
*/
public function calculate_retry_delay($attempt_number, $strategy = self::STRATEGY_EXPONENTIAL, $options = [])
{
$base_delay = $options['base_delay'] ?? self::DEFAULT_BASE_DELAY;
$max_delay = $options['max_delay'] ?? self::DEFAULT_MAX_DELAY;
$multiplier = $options['multiplier'] ?? self::DEFAULT_BACKOFF_MULTIPLIER;
$jitter_enabled = $options['jitter'] ?? self::DEFAULT_JITTER_ENABLED;
switch ($strategy) {
case self::STRATEGY_EXPONENTIAL:
$delay = $base_delay * pow($multiplier, $attempt_number - 1);
break;
case self::STRATEGY_LINEAR:
$delay = $base_delay * $attempt_number;
break;
case self::STRATEGY_FIXED:
$delay = $base_delay;
break;
case self::STRATEGY_FIBONACCI:
$delay = $this->fibonacci_delay($attempt_number, $base_delay);
break;
default:
$delay = $base_delay * pow($multiplier, $attempt_number - 1);
}
// Cap at maximum delay
$delay = min($delay, $max_delay);
// Add jitter to prevent thundering herd
if ($jitter_enabled) {
$delay = $this->add_jitter($delay);
}
return (int)$delay;
}
/**
* Determine if an error is retryable
*
* @param string $error_type
* @param string $error_message
* @param int $http_status_code
* @return bool
*/
public function is_retryable_error($error_type, $error_message = '', $http_status_code = null)
{
// Check explicit non-retryable errors first
if (in_array($error_type, $this->non_retryable_errors)) {
return false;
}
// Check explicit retryable errors
if (in_array($error_type, $this->retryable_errors)) {
return true;
}
// Check HTTP status codes
if ($http_status_code !== null) {
return $this->is_retryable_http_status($http_status_code);
}
// Check error message patterns
return $this->is_retryable_error_message($error_message);
}
/**
* Execute operation with retry logic
*
* @param callable $operation
* @param array $retry_config
* @param array $context
* @return array
*/
public function execute_with_retry(callable $operation, $retry_config = [], $context = [])
{
$max_attempts = $retry_config['max_attempts'] ?? self::DEFAULT_MAX_ATTEMPTS;
$strategy = $retry_config['strategy'] ?? self::STRATEGY_EXPONENTIAL;
$circuit_breaker_key = $context['circuit_breaker_key'] ?? null;
// Check circuit breaker if enabled
if ($circuit_breaker_key && $this->is_circuit_breaker_open($circuit_breaker_key)) {
return [
'success' => false,
'message' => 'Circuit breaker is open',
'error_type' => 'circuit_breaker_open',
'attempts' => 0
];
}
$last_error = null;
for ($attempt = 1; $attempt <= $max_attempts; $attempt++) {
try {
// Record attempt
$this->record_retry_attempt($context, $attempt);
// Execute operation
$result = $operation($attempt);
// Success - record and reset circuit breaker
if ($result['success']) {
$this->record_retry_success($context, $attempt);
if ($circuit_breaker_key) {
$this->record_circuit_breaker_success($circuit_breaker_key);
}
return array_merge($result, ['attempts' => $attempt]);
}
$last_error = $result;
// Check if error is retryable
if (!$this->is_retryable_error(
$result['error_type'] ?? 'unknown',
$result['message'] ?? '',
$result['http_status'] ?? null
)) {
break;
}
// Don't delay after last attempt
if ($attempt < $max_attempts) {
$delay = $this->calculate_retry_delay($attempt, $strategy, $retry_config);
$this->record_retry_delay($context, $attempt, $delay);
sleep($delay);
}
} catch (\Exception $e) {
$last_error = [
'success' => false,
'message' => $e->getMessage(),
'error_type' => 'exception',
'exception' => $e
];
// Record exception attempt
$this->record_retry_exception($context, $attempt, $e);
// Check if exception is retryable
if (!$this->is_retryable_exception($e)) {
break;
}
if ($attempt < $max_attempts) {
$delay = $this->calculate_retry_delay($attempt, $strategy, $retry_config);
sleep($delay);
}
}
}
// All retries failed
$this->record_retry_failure($context, $max_attempts, $last_error);
// Update circuit breaker on failure
if ($circuit_breaker_key) {
$this->record_circuit_breaker_failure($circuit_breaker_key);
}
return array_merge($last_error, ['attempts' => $max_attempts]);
}
/**
* Get retry statistics for monitoring
*
* @param array $filters
* @return array
*/
public function get_retry_statistics($filters = [])
{
return [
'total_attempts' => $this->model->count_retry_attempts($filters),
'total_successes' => $this->model->count_retry_successes($filters),
'total_failures' => $this->model->count_retry_failures($filters),
'success_rate' => $this->calculate_retry_success_rate($filters),
'average_attempts' => $this->model->get_average_retry_attempts($filters),
'retry_distribution' => $this->model->get_retry_attempt_distribution($filters),
'error_types' => $this->model->get_retry_error_types($filters),
'circuit_breaker_states' => $this->get_circuit_breaker_states()
];
}
/**
* Check circuit breaker state
*
* @param string $circuit_key
* @return bool
*/
public function is_circuit_breaker_open($circuit_key)
{
$circuit_state = $this->get_circuit_breaker_state($circuit_key);
switch ($circuit_state['state']) {
case self::CIRCUIT_OPEN:
// Check if timeout has passed
if (time() - $circuit_state['opened_at'] >= self::CIRCUIT_BREAKER_TIMEOUT) {
$this->set_circuit_breaker_state($circuit_key, self::CIRCUIT_HALF_OPEN);
return false;
}
return true;
case self::CIRCUIT_HALF_OPEN:
return false;
case self::CIRCUIT_CLOSED:
default:
return false;
}
}
/**
* Record circuit breaker failure
*
* @param string $circuit_key
*/
public function record_circuit_breaker_failure($circuit_key)
{
$circuit_state = $this->get_circuit_breaker_state($circuit_key);
$failure_count = $circuit_state['failure_count'] + 1;
if ($failure_count >= self::CIRCUIT_BREAKER_FAILURE_THRESHOLD) {
$this->set_circuit_breaker_state($circuit_key, self::CIRCUIT_OPEN, [
'failure_count' => $failure_count,
'opened_at' => time()
]);
$this->error_handler->log_error(
ErrorHandler::CATEGORY_SYSTEM,
'CIRCUIT_BREAKER_OPENED',
"Circuit breaker opened for {$circuit_key} after {$failure_count} failures",
['circuit_key' => $circuit_key],
ErrorHandler::SEVERITY_HIGH
);
} else {
$this->update_circuit_breaker_failure_count($circuit_key, $failure_count);
}
}
/**
* Record circuit breaker success
*
* @param string $circuit_key
*/
public function record_circuit_breaker_success($circuit_key)
{
$circuit_state = $this->get_circuit_breaker_state($circuit_key);
if ($circuit_state['state'] === self::CIRCUIT_HALF_OPEN) {
$success_count = $circuit_state['success_count'] + 1;
if ($success_count >= self::CIRCUIT_BREAKER_SUCCESS_THRESHOLD) {
$this->set_circuit_breaker_state($circuit_key, self::CIRCUIT_CLOSED, [
'success_count' => 0,
'failure_count' => 0
]);
log_activity("Circuit breaker closed for {$circuit_key} after successful operations");
} else {
$this->update_circuit_breaker_success_count($circuit_key, $success_count);
}
} else {
// Reset failure count on success
$this->update_circuit_breaker_failure_count($circuit_key, 0);
}
}
/**
* Get optimal retry configuration for operation type
*
* @param string $operation_type
* @param string $entity_type
* @return array
*/
public function get_optimal_retry_config($operation_type, $entity_type = null)
{
$base_config = [
'max_attempts' => self::DEFAULT_MAX_ATTEMPTS,
'strategy' => self::STRATEGY_EXPONENTIAL,
'base_delay' => self::DEFAULT_BASE_DELAY,
'max_delay' => self::DEFAULT_MAX_DELAY,
'multiplier' => self::DEFAULT_BACKOFF_MULTIPLIER,
'jitter' => self::DEFAULT_JITTER_ENABLED
];
// Customize based on operation type
switch ($operation_type) {
case 'api_call':
$base_config['max_attempts'] = 3;
$base_config['base_delay'] = 2;
$base_config['max_delay'] = 60;
break;
case 'database_operation':
$base_config['max_attempts'] = 2;
$base_config['strategy'] = self::STRATEGY_FIXED;
$base_config['base_delay'] = 1;
break;
case 'file_operation':
$base_config['max_attempts'] = 3;
$base_config['strategy'] = self::STRATEGY_LINEAR;
$base_config['base_delay'] = 1;
break;
case 'sync_operation':
$base_config['max_attempts'] = 5;
$base_config['base_delay'] = 5;
$base_config['max_delay'] = 300;
break;
}
// Further customize based on entity type
if ($entity_type) {
switch ($entity_type) {
case 'customer':
$base_config['max_attempts'] = min($base_config['max_attempts'], 3);
break;
case 'invoice':
$base_config['max_attempts'] = 5; // More important
$base_config['max_delay'] = 600;
break;
case 'product':
$base_config['max_attempts'] = 3;
break;
}
}
return $base_config;
}
/**
* Add jitter to delay to prevent thundering herd
*
* @param float $delay
* @param float $jitter_factor
* @return float
*/
protected function add_jitter($delay, $jitter_factor = 0.1)
{
$jitter_range = $delay * $jitter_factor;
$jitter = (mt_rand() / mt_getrandmax()) * $jitter_range * 2 - $jitter_range;
return max(0, $delay + $jitter);
}
/**
* Calculate fibonacci delay
*
* @param int $n
* @param float $base_delay
* @return float
*/
protected function fibonacci_delay($n, $base_delay)
{
if ($n <= 1) return $base_delay;
if ($n == 2) return $base_delay;
$a = $base_delay;
$b = $base_delay;
for ($i = 3; $i <= $n; $i++) {
$temp = $a + $b;
$a = $b;
$b = $temp;
}
return $b;
}
/**
* Check if HTTP status code is retryable
*
* @param int $status_code
* @return bool
*/
protected function is_retryable_http_status($status_code)
{
// 5xx server errors are generally retryable
if ($status_code >= 500) {
return true;
}
// Some 4xx errors are retryable
$retryable_4xx = [408, 429, 423, 424]; // Request timeout, rate limit, locked, failed dependency
return in_array($status_code, $retryable_4xx);
}
/**
* Check if error message indicates retryable error
*
* @param string $error_message
* @return bool
*/
protected function is_retryable_error_message($error_message)
{
$retryable_patterns = [
'/timeout/i',
'/connection.*failed/i',
'/network.*error/i',
'/temporary.*unavailable/i',
'/service.*unavailable/i',
'/rate.*limit/i',
'/too many requests/i',
'/server.*error/i'
];
foreach ($retryable_patterns as $pattern) {
if (preg_match($pattern, $error_message)) {
return true;
}
}
return false;
}
/**
* Check if exception is retryable
*
* @param \Exception $exception
* @return bool
*/
protected function is_retryable_exception($exception)
{
$retryable_exceptions = [
'PDOException',
'mysqli_sql_exception',
'RedisException',
'cURLException',
'TimeoutException'
];
$exception_class = get_class($exception);
return in_array($exception_class, $retryable_exceptions) ||
$this->is_retryable_error_message($exception->getMessage());
}
/**
* Record retry attempt
*
* @param array $context
* @param int $attempt
*/
protected function record_retry_attempt($context, $attempt)
{
$this->model->record_retry_attempt([
'operation_type' => $context['operation_type'] ?? 'unknown',
'entity_type' => $context['entity_type'] ?? null,
'entity_id' => $context['entity_id'] ?? null,
'attempt_number' => $attempt,
'attempted_at' => date('Y-m-d H:i:s'),
'context' => json_encode($context)
]);
}
/**
* Record retry success
*
* @param array $context
* @param int $total_attempts
*/
protected function record_retry_success($context, $total_attempts)
{
$this->model->record_retry_success([
'operation_type' => $context['operation_type'] ?? 'unknown',
'entity_type' => $context['entity_type'] ?? null,
'entity_id' => $context['entity_id'] ?? null,
'total_attempts' => $total_attempts,
'succeeded_at' => date('Y-m-d H:i:s'),
'context' => json_encode($context)
]);
}
/**
* Record retry failure
*
* @param array $context
* @param int $total_attempts
* @param array $last_error
*/
protected function record_retry_failure($context, $total_attempts, $last_error)
{
$this->model->record_retry_failure([
'operation_type' => $context['operation_type'] ?? 'unknown',
'entity_type' => $context['entity_type'] ?? null,
'entity_id' => $context['entity_id'] ?? null,
'total_attempts' => $total_attempts,
'failed_at' => date('Y-m-d H:i:s'),
'last_error' => json_encode($last_error),
'context' => json_encode($context)
]);
}
/**
* Get circuit breaker state
*
* @param string $circuit_key
* @return array
*/
protected function get_circuit_breaker_state($circuit_key)
{
return $this->model->get_circuit_breaker_state($circuit_key) ?: [
'state' => self::CIRCUIT_CLOSED,
'failure_count' => 0,
'success_count' => 0,
'opened_at' => null
];
}
/**
* Set circuit breaker state
*
* @param string $circuit_key
* @param string $state
* @param array $additional_data
*/
protected function set_circuit_breaker_state($circuit_key, $state, $additional_data = [])
{
$data = array_merge([
'circuit_key' => $circuit_key,
'state' => $state,
'updated_at' => date('Y-m-d H:i:s')
], $additional_data);
$this->model->set_circuit_breaker_state($circuit_key, $data);
}
/**
* Calculate retry success rate
*
* @param array $filters
* @return float
*/
protected function calculate_retry_success_rate($filters)
{
$total_attempts = $this->model->count_retry_attempts($filters);
$total_successes = $this->model->count_retry_successes($filters);
return $total_attempts > 0 ? ($total_successes / $total_attempts) * 100 : 0;
}
/**
* Get all circuit breaker states
*
* @return array
*/
protected function get_circuit_breaker_states()
{
return $this->model->get_all_circuit_breaker_states();
}
}