/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ CI = &get_instance(); // Load required models and libraries $this->CI->load->model('desk_moloni/desk_moloni_sync_queue_model', 'sync_queue_model'); $this->CI->load->model('desk_moloni/desk_moloni_sync_log_model', 'sync_log_model'); $this->CI->load->library('desk_moloni/moloni_api_client'); $this->CI->load->library('desk_moloni/client_sync_service'); $this->CI->load->library('desk_moloni/invoice_sync_service'); // Generate unique worker ID $this->worker_id = uniqid('worker_', true); $this->worker_pid = getmypid(); // Set memory and execution limits $this->memory_limit = $this->convert_to_bytes(ini_get('memory_limit')); $this->execution_timeout = (int) get_option('desk_moloni_worker_timeout', 300); // 5 minutes default // Initialize worker lock file $this->worker_lock_file = APPPATH . "logs/desk_moloni_worker_{$this->worker_id}.lock"; // Register task handlers $this->register_task_handlers(); // Register shutdown handler register_shutdown_function([$this, 'shutdown_handler']); log_message('info', "TaskWorker {$this->worker_id} initialized with PID {$this->worker_pid}"); } /** * Start the worker process * * @param array $options Worker configuration options * @return void */ public function start($options = []) { $this->is_running = true; // Process options if (isset($options['max_tasks'])) { $this->max_tasks_per_worker = (int) $options['max_tasks']; } // Create worker lock file $this->create_lock_file(); log_message('info', "TaskWorker {$this->worker_id} starting..."); try { $this->worker_loop(); } catch (Exception $e) { log_message('error', "TaskWorker {$this->worker_id} error: " . $e->getMessage()); } finally { $this->cleanup(); } } /** * Stop the worker process */ public function stop() { $this->is_running = false; log_message('info', "TaskWorker {$this->worker_id} stopping..."); } /** * Main worker loop */ private function worker_loop() { $last_heartbeat = time(); while ($this->is_running && $this->task_count < $this->max_tasks_per_worker) { // Check memory usage if ($this->is_memory_limit_exceeded()) { log_message('warning', "TaskWorker {$this->worker_id} memory limit exceeded, stopping"); break; } // Update heartbeat if (time() - $last_heartbeat >= $this->heartbeat_interval) { $this->update_heartbeat(); $last_heartbeat = time(); } // Get next task from queue $task = $this->CI->sync_queue_model->get_next_task($this->worker_id); if (!$task) { // No tasks available, sleep briefly sleep(1); continue; } // Execute task $this->execute_task($task); $this->task_count++; // Brief pause between tasks usleep(100000); // 0.1 second } log_message('info', "TaskWorker {$this->worker_id} completed {$this->task_count} tasks"); } /** * Execute a single task * * @param array $task Task data */ private function execute_task($task) { $this->current_task = $task; $start_time = microtime(true); try { // Update task status to processing $this->CI->sync_queue_model->update_task_status($task['id'], 'processing', [ 'worker_id' => $this->worker_id, 'started_at' => date('Y-m-d H:i:s'), 'pid' => $this->worker_pid ]); log_message('info', "TaskWorker {$this->worker_id} executing task {$task['id']} ({$task['task_type']})"); // Set execution timeout set_time_limit($this->execution_timeout); // Get appropriate task handler $handler = $this->get_task_handler($task['task_type']); if (!$handler) { throw new Exception("No handler found for task type: {$task['task_type']}"); } // Execute task $result = call_user_func($handler, $task); $execution_time = microtime(true) - $start_time; // Update task as completed $this->CI->sync_queue_model->update_task_status($task['id'], 'completed', [ 'completed_at' => date('Y-m-d H:i:s'), 'execution_time' => $execution_time, 'result' => json_encode($result), 'worker_id' => $this->worker_id ]); // Log successful execution $this->CI->sync_log_model->log_event([ 'task_id' => $task['id'], 'event_type' => 'task_completed', 'entity_type' => $task['entity_type'], 'entity_id' => $task['entity_id'], 'message' => "Task executed successfully by worker {$this->worker_id}", 'execution_time' => $execution_time, 'worker_id' => $this->worker_id ]); log_message('info', "TaskWorker {$this->worker_id} completed task {$task['id']} in " . number_format($execution_time, 3) . "s"); } catch (Exception $e) { $execution_time = microtime(true) - $start_time; // Update task as failed $this->CI->sync_queue_model->update_task_status($task['id'], 'failed', [ 'failed_at' => date('Y-m-d H:i:s'), 'error_message' => $e->getMessage(), 'execution_time' => $execution_time, 'worker_id' => $this->worker_id, 'retry_count' => ($task['retry_count'] ?? 0) + 1 ]); // Log error $this->CI->sync_log_model->log_event([ 'task_id' => $task['id'], 'event_type' => 'task_failed', 'entity_type' => $task['entity_type'], 'entity_id' => $task['entity_id'], 'message' => "Task failed: " . $e->getMessage(), 'log_level' => 'error', 'execution_time' => $execution_time, 'worker_id' => $this->worker_id ]); log_message('error', "TaskWorker {$this->worker_id} failed task {$task['id']}: " . $e->getMessage()); // Schedule retry if appropriate $this->schedule_retry($task, $e); } $this->current_task = null; } /** * Register task handlers */ private function register_task_handlers() { $this->task_handlers = [ 'client_sync' => [$this, 'handle_client_sync'], 'invoice_sync' => [$this, 'handle_invoice_sync'], 'oauth_refresh' => [$this, 'handle_oauth_refresh'], 'cleanup' => [$this, 'handle_cleanup'], 'notification' => [$this, 'handle_notification'], 'bulk_sync' => [$this, 'handle_bulk_sync'], 'data_validation' => [$this, 'handle_data_validation'], 'mapping_discovery' => [$this, 'handle_mapping_discovery'] ]; } /** * Get task handler for task type * * @param string $task_type Task type * @return callable|null Handler function */ private function get_task_handler($task_type) { return $this->task_handlers[$task_type] ?? null; } /** * Handle client synchronization task * * @param array $task Task data * @return array Result */ private function handle_client_sync($task) { $client_id = $task['entity_id']; $payload = json_decode($task['payload'], true) ?? []; return $this->CI->client_sync_service->sync_client($client_id, $payload); } /** * Handle invoice synchronization task * * @param array $task Task data * @return array Result */ private function handle_invoice_sync($task) { $invoice_id = $task['entity_id']; $payload = json_decode($task['payload'], true) ?? []; return $this->CI->invoice_sync_service->sync_invoice($invoice_id, $payload); } /** * Handle OAuth token refresh task * * @param array $task Task data * @return array Result */ private function handle_oauth_refresh($task) { $this->CI->load->library('desk_moloni/moloni_oauth'); $success = $this->CI->moloni_oauth->refresh_access_token(); return [ 'success' => $success, 'refreshed_at' => date('Y-m-d H:i:s') ]; } /** * Handle cleanup task * * @param array $task Task data * @return array Result */ private function handle_cleanup($task) { $payload = json_decode($task['payload'], true) ?? []; $cleanup_type = $payload['type'] ?? 'general'; $cleaned = 0; switch ($cleanup_type) { case 'logs': $days = $payload['days'] ?? 30; $cleaned = $this->CI->sync_log_model->cleanup_old_logs($days); break; case 'queue': $status = $payload['status'] ?? 'completed'; $cleaned = $this->CI->sync_queue_model->cleanup_old_tasks($status); break; default: // General cleanup $cleaned += $this->CI->sync_log_model->cleanup_old_logs(30); $cleaned += $this->CI->sync_queue_model->cleanup_old_tasks('completed'); } return [ 'cleanup_type' => $cleanup_type, 'items_cleaned' => $cleaned ]; } /** * Handle notification task * * @param array $task Task data * @return array Result */ private function handle_notification($task) { // Placeholder for notification handling return [ 'notification_sent' => false, 'message' => 'Notification handling not yet implemented' ]; } /** * Handle bulk synchronization task * * @param array $task Task data * @return array Result */ private function handle_bulk_sync($task) { $payload = json_decode($task['payload'], true) ?? []; $entity_type = $payload['entity_type'] ?? 'all'; $batch_size = $payload['batch_size'] ?? 50; $processed = 0; $errors = 0; // Implementation would depend on entity type // For now, return a placeholder result return [ 'entity_type' => $entity_type, 'batch_size' => $batch_size, 'processed' => $processed, 'errors' => $errors ]; } /** * Handle data validation task * * @param array $task Task data * @return array Result */ private function handle_data_validation($task) { // Placeholder for data validation return [ 'validated' => true, 'issues_found' => 0 ]; } /** * Handle mapping discovery task * * @param array $task Task data * @return array Result */ private function handle_mapping_discovery($task) { $payload = json_decode($task['payload'], true) ?? []; $entity_type = $payload['entity_type'] ?? 'client'; $this->CI->load->model('desk_moloni/desk_moloni_mapping_model', 'mapping_model'); $discovered_mappings = $this->CI->mapping_model->discover_mappings($entity_type, true); return [ 'entity_type' => $entity_type, 'discovered_count' => count($discovered_mappings), 'mappings' => $discovered_mappings ]; } /** * Schedule task retry * * @param array $task Task data * @param Exception $error Error that caused failure */ private function schedule_retry($task, $error) { $retry_count = ($task['retry_count'] ?? 0) + 1; $max_retries = (int) get_option('desk_moloni_max_retries', 3); if ($retry_count <= $max_retries) { // Calculate backoff delay $delay = min(pow(2, $retry_count) * 60, 3600); // Exponential backoff, max 1 hour $this->CI->sync_queue_model->schedule_retry($task['id'], $delay); log_message('info', "TaskWorker {$this->worker_id} scheduled retry {$retry_count}/{$max_retries} " . "for task {$task['id']} in {$delay}s"); } else { log_message('warning', "TaskWorker {$this->worker_id} task {$task['id']} exceeded max retries"); } } /** * Create worker lock file */ private function create_lock_file() { $lock_data = [ 'worker_id' => $this->worker_id, 'pid' => $this->worker_pid, 'started_at' => date('Y-m-d H:i:s'), 'last_heartbeat' => time() ]; file_put_contents($this->worker_lock_file, json_encode($lock_data)); } /** * Update worker heartbeat */ private function update_heartbeat() { if (file_exists($this->worker_lock_file)) { $lock_data = json_decode(file_get_contents($this->worker_lock_file), true); $lock_data['last_heartbeat'] = time(); $lock_data['task_count'] = $this->task_count; $lock_data['current_task'] = $this->current_task['id'] ?? null; file_put_contents($this->worker_lock_file, json_encode($lock_data)); } } /** * Check if memory limit is exceeded * * @return bool Memory limit exceeded */ private function is_memory_limit_exceeded() { if ($this->memory_limit === -1) { return false; // No memory limit } $current_usage = memory_get_usage(true); $percentage = ($current_usage / $this->memory_limit) * 100; return $percentage > 80; // Stop at 80% memory usage } /** * Convert memory limit to bytes * * @param string $val Memory limit string * @return int Bytes */ private function convert_to_bytes($val) { if ($val === '-1') { return -1; } $val = trim($val); $last = strtolower($val[strlen($val) - 1]); $val = (int) $val; switch ($last) { case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } /** * Cleanup worker resources */ private function cleanup() { // Remove lock file if (file_exists($this->worker_lock_file)) { unlink($this->worker_lock_file); } // Release any pending tasks assigned to this worker if ($this->current_task) { $this->CI->sync_queue_model->release_task($this->current_task['id']); } log_message('info', "TaskWorker {$this->worker_id} cleanup completed"); } /** * Shutdown handler */ public function shutdown_handler() { if ($this->is_running) { log_message('warning', "TaskWorker {$this->worker_id} unexpected shutdown"); $this->cleanup(); } } /** * Get worker status * * @return array Worker status */ public function get_status() { $status = [ 'worker_id' => $this->worker_id, 'pid' => $this->worker_pid, 'is_running' => $this->is_running, 'task_count' => $this->task_count, 'max_tasks' => $this->max_tasks_per_worker, 'current_task' => $this->current_task, 'memory_usage' => memory_get_usage(true), 'memory_limit' => $this->memory_limit, 'execution_timeout' => $this->execution_timeout ]; if (file_exists($this->worker_lock_file)) { $lock_data = json_decode(file_get_contents($this->worker_lock_file), true); $status['lock_data'] = $lock_data; } return $status; } /** * Check if worker is healthy * * @return bool Worker health status */ public function is_healthy() { // Check if lock file exists and is recent if (!file_exists($this->worker_lock_file)) { return false; } $lock_data = json_decode(file_get_contents($this->worker_lock_file), true); $last_heartbeat = $lock_data['last_heartbeat'] ?? 0; // Worker is healthy if heartbeat is within 2 intervals return (time() - $last_heartbeat) < ($this->heartbeat_interval * 2); } }