fix: replace SSE with polling in live feed to stop blocking FPM workers

SSE kept a PHP-FPM worker open for 28s per connection, blocking the whole
server with only 20 workers shared across all vhosts. Polling with
setInterval(3000) releases the worker on each request immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-22 11:37:21 -05:00
co-authored by Claude Sonnet 4.6
parent c702dbadc1
commit 483cc4797e
+27 -19
View File
@@ -115,7 +115,7 @@ function togglePause() {
paused = !paused; paused = !paused;
document.getElementById('btnPause').innerHTML = paused ? '&#9654; Reanudar' : '&#9646;&#9646; Pausar'; document.getElementById('btnPause').innerHTML = paused ? '&#9654; Reanudar' : '&#9646;&#9646; Pausar';
document.getElementById('dot').className = 'dot' + (paused ? ' paused' : ''); document.getElementById('dot').className = 'dot' + (paused ? ' paused' : '');
if (paused) { document.getElementById('status').textContent = 'En pausa'; es && es.close(); } if (paused) { document.getElementById('status').textContent = 'En pausa'; clearInterval(pollTimer); pollTimer = null; }
else { connect(); } else { connect(); }
} }
@@ -154,17 +154,28 @@ function addCards(rows) {
} }
} }
function connect() { let lastId = 0;
es = new EventSource('/admin/webhook/stream/sse'); let pollTimer = null;
es.onopen = () => {
async function poll() {
if (paused) return;
try {
const r = await fetch('/admin/webhook/stream?after=' + lastId + (lastId === 0 ? '&init=1' : ''));
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
if (d.last_id !== undefined) { lastId = d.last_id; }
if (d.rows && d.rows.length) { lastId = d.rows[d.rows.length-1].id; addCards(d.rows); }
document.getElementById('dot').className = 'dot'; document.getElementById('dot').className = 'dot';
document.getElementById('status').textContent = 'Conectado — tiempo real (SSE)'; document.getElementById('status').textContent = 'Conectado — polling cada 3s';
}; } catch(e) {
es.onmessage = e => { if (!paused) try { addCards(JSON.parse(e.data)); } catch(_) {} };
es.onerror = () => {
document.getElementById('dot').className = 'dot paused'; document.getElementById('dot').className = 'dot paused';
document.getElementById('status').textContent = 'Reconectando...'; document.getElementById('status').textContent = 'Error de conexion, reintentando...';
}; }
}
function connect() {
poll();
pollTimer = setInterval(poll, 3000);
} }
function esc(s) { function esc(s) {
@@ -181,7 +192,7 @@ async function showRaw(id) {
} }
function closeModal() { document.getElementById('modal').style.display = 'none'; } function closeModal() { document.getElementById('modal').style.display = 'none'; }
document.addEventListener('keydown', e => { if(e.key==='Escape') closeModal(); }); document.addEventListener('keydown', e => { if(e.key==='Escape') closeModal(); });
window.addEventListener('pagehide', () => { es && es.close(); }); window.addEventListener('pagehide', () => { clearInterval(pollTimer); });
connect(); connect();
</script> </script>
@@ -261,13 +272,10 @@ HTML;
try { try {
if ($isInit) { if ($isInit) {
// En init devolvemos solo el último ID (sin datos) para anclar $row = db()->query('SELECT id FROM webhook_logs ORDER BY id DESC LIMIT 1')->fetch();
$stmt = db()->query('SELECT id FROM webhook_logs ORDER BY id DESC LIMIT 1'); $lastId = $row ? (int)$row['id'] : 0;
$row = $stmt->fetch();
$afterId = $row ? (int)$row['id'] : 0;
// Devolvemos array vacío — solo queremos anclar lastId en el cliente
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
echo json_encode([]); echo json_encode(['rows' => [], 'last_id' => $lastId]);
exit; exit;
} }
@@ -283,12 +291,12 @@ HTML;
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (\PDOException $e) { } catch (\PDOException $e) {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
echo '[]'; echo json_encode(['rows' => [], 'last_id' => $afterId]);
exit; exit;
} }
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
echo json_encode($rows, JSON_UNESCAPED_UNICODE); echo json_encode(['rows' => $rows, 'last_id' => $rows ? (int)end($rows)['id'] : $afterId], JSON_UNESCAPED_UNICODE);
exit; exit;
} }