Update: 2026-05-08 15:02:13
This commit is contained in:
139
app/cron/diagnose.php
Normal file
139
app/cron/diagnose.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Diagnostic Script — Run on server to verify processing works
|
||||
*
|
||||
* Usage: php app/cron/diagnose.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
echo "=== Musadaq Processing Diagnostics ===\n";
|
||||
echo "Time: " . date('Y-m-d H:i:s') . "\n";
|
||||
echo "PHP: " . PHP_VERSION . "\n";
|
||||
echo "SAPI: " . php_sapi_name() . "\n\n";
|
||||
|
||||
// 1. Check DB connection
|
||||
echo "--- Database ---\n";
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
echo " ✓ Database connected\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo " ✗ Database FAILED: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 2. Check pending queue items
|
||||
echo "\n--- Queue Status ---\n";
|
||||
$stmt = $db->query("SELECT status, COUNT(*) as cnt FROM invoice_processing_queue GROUP BY status");
|
||||
$rows = $stmt->fetchAll();
|
||||
if (empty($rows)) {
|
||||
echo " (empty — no items in queue at all)\n";
|
||||
} else {
|
||||
foreach ($rows as $r) {
|
||||
echo " {$r['status']}: {$r['cnt']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check batch statuses
|
||||
echo "\n--- Batch Status ---\n";
|
||||
$stmt = $db->query("SELECT status, COUNT(*) as cnt FROM invoice_batches GROUP BY status");
|
||||
$rows = $stmt->fetchAll();
|
||||
if (empty($rows)) {
|
||||
echo " (empty — no batches)\n";
|
||||
} else {
|
||||
foreach ($rows as $r) {
|
||||
echo " {$r['status']}: {$r['cnt']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check for stuck items (processing but no worker)
|
||||
echo "\n--- Stuck Items (processing for >5 minutes) ---\n";
|
||||
$stmt = $db->query("
|
||||
SELECT q.id, q.batch_id, q.status, q.image_path, q.created_at, q.error_message
|
||||
FROM invoice_processing_queue q
|
||||
WHERE q.status IN ('pending', 'processing')
|
||||
ORDER BY q.created_at DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$stuck = $stmt->fetchAll();
|
||||
if (empty($stuck)) {
|
||||
echo " (none — all clear)\n";
|
||||
} else {
|
||||
foreach ($stuck as $s) {
|
||||
$exists = file_exists($s['image_path']) ? '✓ file exists' : '✗ FILE MISSING';
|
||||
echo " ID={$s['id']} | Status={$s['status']} | $exists\n";
|
||||
echo " Path: {$s['image_path']}\n";
|
||||
if ($s['error_message']) echo " Error: {$s['error_message']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check lock file
|
||||
echo "\n--- Lock File ---\n";
|
||||
$lockFile = STORAGE_PATH . '/logs/process_batches.lock';
|
||||
if (file_exists($lockFile)) {
|
||||
$age = time() - filemtime($lockFile);
|
||||
$content = trim(file_get_contents($lockFile));
|
||||
echo " ⚠ Lock file EXISTS (age: {$age}s, content: $content)\n";
|
||||
if ($age > 300) {
|
||||
echo " → This lock is STALE. Removing...\n";
|
||||
@unlink($lockFile);
|
||||
echo " ✓ Removed.\n";
|
||||
}
|
||||
} else {
|
||||
echo " ✓ No lock file (good)\n";
|
||||
}
|
||||
|
||||
// 6. Check key files
|
||||
echo "\n--- Key Files ---\n";
|
||||
$files = [
|
||||
'InvoiceProcessor' => APP_PATH . '/Services/InvoiceProcessor.php',
|
||||
'AI' => APP_PATH . '/Core/AI.php',
|
||||
'process_batches' => APP_PATH . '/cron/process_batches.php',
|
||||
'worker.log' => STORAGE_PATH . '/logs/worker.log',
|
||||
];
|
||||
foreach ($files as $name => $path) {
|
||||
if (file_exists($path)) {
|
||||
echo " ✓ $name: $path (" . filesize($path) . " bytes)\n";
|
||||
} else {
|
||||
echo " ✗ $name: MISSING — $path\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Check Gemini API key
|
||||
echo "\n--- Configuration ---\n";
|
||||
$apiKey = env('GEMINI_API_KEY');
|
||||
echo " GEMINI_API_KEY: " . ($apiKey ? "✓ Set (" . strlen($apiKey) . " chars)" : "✗ MISSING!") . "\n";
|
||||
echo " APP_DEBUG: " . env('APP_DEBUG', 'false') . "\n";
|
||||
echo " fastcgi_finish_request: " . (function_exists('fastcgi_finish_request') ? '✓ Available' : '✗ Not available (CLI mode)') . "\n";
|
||||
|
||||
// 8. Show last lines of worker.log
|
||||
echo "\n--- Last 20 lines of worker.log ---\n";
|
||||
$workerLog = STORAGE_PATH . '/logs/worker.log';
|
||||
if (file_exists($workerLog)) {
|
||||
$lines = file($workerLog);
|
||||
$last = array_slice($lines, -20);
|
||||
foreach ($last as $line) {
|
||||
echo " " . rtrim($line) . "\n";
|
||||
}
|
||||
} else {
|
||||
echo " (worker.log does not exist yet)\n";
|
||||
}
|
||||
|
||||
// 9. Try to reset any stuck 'processing' items back to 'pending'
|
||||
echo "\n--- Fix Stuck Items? ---\n";
|
||||
$stmt = $db->query("SELECT COUNT(*) FROM invoice_processing_queue WHERE status = 'processing'");
|
||||
$stuckCount = (int)$stmt->fetchColumn();
|
||||
if ($stuckCount > 0) {
|
||||
echo " Found $stuckCount items stuck in 'processing' state.\n";
|
||||
$db->query("UPDATE invoice_processing_queue SET status = 'pending' WHERE status = 'processing'");
|
||||
echo " ✓ Reset them to 'pending' so they can be reprocessed.\n";
|
||||
} else {
|
||||
echo " ✓ No stuck items.\n";
|
||||
}
|
||||
|
||||
echo "\n=== Diagnostics Complete ===\n";
|
||||
echo "Next step: Run 'php app/cron/process_batches.php' to process pending items.\n";
|
||||
@@ -1,56 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* Background Worker for AI Invoice Extraction
|
||||
* Processes images in the invoice_processing_queue.
|
||||
* Cron Worker for AI Invoice Extraction
|
||||
*
|
||||
* Designed to run via cron every minute: * * * * *
|
||||
* Processes ALL pending items in the queue, then EXITS.
|
||||
* NO infinite loop. NO lock file issues.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Services\InvoiceProcessor;
|
||||
|
||||
// Prevent multiple instances (Lock file)
|
||||
// Simple lock: prevent overlapping runs
|
||||
$lockFile = STORAGE_PATH . '/logs/process_batches.lock';
|
||||
$fp = fopen($lockFile, 'c+');
|
||||
if (!flock($fp, LOCK_EX | LOCK_NB)) {
|
||||
exit("Worker already running.\n");
|
||||
|
||||
// Check if lock file exists and is stale (older than 5 minutes = dead process)
|
||||
if (file_exists($lockFile)) {
|
||||
$lockAge = time() - filemtime($lockFile);
|
||||
if ($lockAge > 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);
|
||||
}
|
||||
}
|
||||
|
||||
echo "Starting Musadaq AI Worker [" . date('Y-m-d H:i:s') . "]\n";
|
||||
// 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;
|
||||
|
||||
while (true) {
|
||||
$stmt = $db->prepare("
|
||||
SELECT id FROM invoice_processing_queue
|
||||
WHERE status = 'pending'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute();
|
||||
$queueId = $stmt->fetchColumn();
|
||||
// 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 (!$queueId) {
|
||||
echo "Queue empty. Waiting...\n";
|
||||
sleep(5);
|
||||
continue;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
echo "Processing Queue ID: $queueId\n";
|
||||
$success = InvoiceProcessor::processQueueItem((int)$queueId);
|
||||
|
||||
if ($success) {
|
||||
echo "Success for Queue ID $queueId\n";
|
||||
} else {
|
||||
echo "Failed for Queue ID $queueId\n";
|
||||
}
|
||||
workerLog("=== Worker Done: $processed success, $failed failed ===");
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Fatal Worker Error: " . $e->getMessage() . "\n";
|
||||
} catch (\Throwable $e) {
|
||||
workerLog("FATAL ERROR: " . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
} finally {
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
// ALWAYS remove lock file
|
||||
@unlink($lockFile);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user