300) { // Stale lock from a crashed process - remove it @unlink($lockFile); workerLog("Removed stale lock file (age: {$lockAge}s)"); } else { workerLog("Worker already running (lock age: {$lockAge}s). Exiting."); exit(0); } } // Create lock file_put_contents($lockFile, getmypid() . "\n" . date('c')); function workerLog(string $msg): void { $line = "[" . date('Y-m-d H:i:s') . "] " . $msg . "\n"; echo $line; // Also write to dedicated log file @file_put_contents(STORAGE_PATH . '/logs/worker.log', $line, FILE_APPEND); } workerLog("=== Musadaq AI Worker Started ==="); try { $db = Database::getInstance(); $processed = 0; $failed = 0; // Get ALL pending items (no infinite loop!) $stmt = $db->prepare(" SELECT id FROM invoice_processing_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT 20 "); $stmt->execute(); $items = $stmt->fetchAll(\PDO::FETCH_COLUMN); if (empty($items)) { workerLog("No pending items. Exiting."); } else { workerLog("Found " . count($items) . " pending item(s)."); foreach ($items as $queueId) { workerLog("Processing Queue ID: $queueId ..."); try { $success = InvoiceProcessor::processQueueItem((int)$queueId); if ($success) { $processed++; workerLog(" ✓ Queue ID $queueId processed successfully."); } else { $failed++; workerLog(" ✗ Queue ID $queueId failed (returned false)."); } } catch (\Throwable $e) { $failed++; workerLog(" ✗ Queue ID $queueId EXCEPTION: " . $e->getMessage()); } } workerLog("=== Worker Done: $processed success, $failed failed ==="); } } catch (\Throwable $e) { workerLog("FATAL ERROR: " . $e->getMessage() . "\n" . $e->getTraceAsString()); } finally { // ALWAYS remove lock file @unlink($lockFile); }