/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 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(); } }