fix: Bridge-EPIPE-Endlosschleife + monitor_events-Schneeball verhindern [appimage]
All checks were successful
Build AppImage / build (push) Successful in 8m24s
All checks were successful
Build AppImage / build (push) Successful in 8m24s
Ursache: Bridge schrieb bei EPIPE-Fehler (Tauri-Pipe geschlossen) einen monitor_event, der selbst wieder EPIPE warf -> uncaughtException-Loop. Ergebnis: 1.082.260 identische Fehler-Eintraege, DB auf 293 MB angewachsen, App hing beim Start am Index-Scan dieser Tabelle. Bridge (scripts/claude-bridge.js): - crashHandlerActive-Flag verhindert Re-Eintreten der Handler - isPipeError() erkennt EPIPE/ERR_STREAM_DESTROYED/ERR_STREAM_WRITE_AFTER_END - Bei Pipe-Fehler: process.exit(0) statt Schreibversuch - stdout/stderr error-Listener als Erstausloeser-Sperre - Alle sendEvent/sendMonitorEvent-Aufrufe in try/catch isoliert Schema (src-tauri/src/db.rs): - Trigger cleanup_old_monitor_events war AFTER INSERT (lief bei jedem Insert) -> bei 1 Mio Zeilen O(n) DELETE pro Event = O(n^2)-Schneeball - Neuer Trigger: WHEN COUNT > 50000, behaelt juengste 30000, loescht >7d alt - DROP TRIGGER vor CREATE damit Migration auf bestehenden DBs greift Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
549727d681
commit
6d3a0d8740
2 changed files with 52 additions and 12 deletions
|
|
@ -1156,6 +1156,10 @@ process.on('exit', () => { cleanupDaemon(); });
|
||||||
// WICHTIG: err.stack ist ein lazy Getter der bei jedem Zugriff neu formatiert wird
|
// WICHTIG: err.stack ist ein lazy Getter der bei jedem Zugriff neu formatiert wird
|
||||||
// und bei V8-OOM selbst einen OOM-Abort auslösen kann. Daher: einmal lesen, kürzen,
|
// und bei V8-OOM selbst einen OOM-Abort auslösen kann. Daher: einmal lesen, kürzen,
|
||||||
// try/catch drumrum — der Handler darf nicht selbst crashen.
|
// try/catch drumrum — der Handler darf nicht selbst crashen.
|
||||||
|
//
|
||||||
|
// Re-Entrancy-Schutz: Wenn der Crash aus dem Schreiben auf stdout/stderr kommt
|
||||||
|
// (EPIPE — Tauri liest nicht mehr), darf der Handler nicht erneut schreiben,
|
||||||
|
// sonst Endlosschleife mit ~5 Events/s in monitor_events (siehe DB-Crash 04/2026).
|
||||||
function safeStack(err) {
|
function safeStack(err) {
|
||||||
try {
|
try {
|
||||||
const raw = (err && err.stack) ? String(err.stack) : '';
|
const raw = (err && err.stack) ? String(err.stack) : '';
|
||||||
|
|
@ -1164,30 +1168,55 @@ function safeStack(err) {
|
||||||
return '[stack nicht lesbar]';
|
return '[stack nicht lesbar]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let crashHandlerActive = false;
|
||||||
|
function isPipeError(err) {
|
||||||
|
if (!err) return false;
|
||||||
|
const code = err.code || (err.cause && err.cause.code);
|
||||||
|
return code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED' || code === 'ERR_STREAM_WRITE_AFTER_END';
|
||||||
|
}
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
|
if (crashHandlerActive) return;
|
||||||
|
crashHandlerActive = true;
|
||||||
try {
|
try {
|
||||||
|
// Pipe ist tot → kein Schreibversuch, sonst Loop. Stiller Exit ist hier korrekt.
|
||||||
|
if (isPipeError(err)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
const msg = (err && err.message) ? String(err.message).slice(0, 500) : String(err).slice(0, 500);
|
const msg = (err && err.message) ? String(err.message).slice(0, 500) : String(err).slice(0, 500);
|
||||||
const stack = safeStack(err);
|
const stack = safeStack(err);
|
||||||
process.stderr.write(`❌ Unbehandelter Fehler: ${msg}\n${stack}\n`);
|
try { process.stderr.write(`❌ Unbehandelter Fehler: ${msg}\n${stack}\n`); } catch {}
|
||||||
sendEvent('bridge-error', { type: 'uncaughtException', message: msg, stack });
|
try { sendEvent('bridge-error', { type: 'uncaughtException', message: msg, stack }); } catch {}
|
||||||
sendMonitorEvent('error', `Bridge Crash: ${msg}`, {});
|
try { sendMonitorEvent('error', `Bridge Crash: ${msg}`, {}); } catch {}
|
||||||
} catch (inner) {
|
} finally {
|
||||||
try { process.stderr.write(`❌ Handler-Fehler: ${inner && inner.message}\n`); } catch {}
|
crashHandlerActive = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
if (crashHandlerActive) return;
|
||||||
|
crashHandlerActive = true;
|
||||||
try {
|
try {
|
||||||
|
if (isPipeError(reason)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
const msg = reason instanceof Error
|
const msg = reason instanceof Error
|
||||||
? String(reason.message).slice(0, 500)
|
? String(reason.message).slice(0, 500)
|
||||||
: String(reason).slice(0, 500);
|
: String(reason).slice(0, 500);
|
||||||
process.stderr.write(`❌ Unhandled Promise Rejection: ${msg}\n`);
|
try { process.stderr.write(`❌ Unhandled Promise Rejection: ${msg}\n`); } catch {}
|
||||||
sendEvent('bridge-error', { type: 'unhandledRejection', message: msg });
|
try { sendEvent('bridge-error', { type: 'unhandledRejection', message: msg }); } catch {}
|
||||||
sendMonitorEvent('error', `Unhandled Rejection: ${msg}`, {});
|
try { sendMonitorEvent('error', `Unhandled Rejection: ${msg}`, {}); } catch {}
|
||||||
} catch (inner) {
|
} finally {
|
||||||
try { process.stderr.write(`❌ Handler-Fehler: ${inner && inner.message}\n`); } catch {}
|
crashHandlerActive = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// stdout/stderr-Streams: EPIPE als Erstauslöser abfangen, damit der globale
|
||||||
|
// uncaughtException-Pfad gar nicht erst antritt.
|
||||||
|
for (const stream of [process.stdout, process.stderr]) {
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
if (isPipeError(err)) process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Bereit-Signal (im stdio-Modus sofort senden, im Daemon-Modus pro Client bei Connect)
|
// Bereit-Signal (im stdio-Modus sofort senden, im Daemon-Modus pro Client bei Connect)
|
||||||
if (!IS_DAEMON) {
|
if (!IS_DAEMON) {
|
||||||
sendEvent('ready', { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });
|
sendEvent('ready', { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });
|
||||||
|
|
|
||||||
|
|
@ -210,12 +210,23 @@ impl Database {
|
||||||
CREATE INDEX IF NOT EXISTS idx_monitor_timestamp ON monitor_events(timestamp DESC);
|
CREATE INDEX IF NOT EXISTS idx_monitor_timestamp ON monitor_events(timestamp DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_monitor_type ON monitor_events(event_type);
|
CREATE INDEX IF NOT EXISTS idx_monitor_type ON monitor_events(event_type);
|
||||||
|
|
||||||
-- Automatisch alte Monitor-Events löschen (älter als 7 Tage)
|
-- Threshold-basierter Cleanup: greift nur wenn die Tabelle mehr als
|
||||||
|
-- 50_000 Zeilen hat, behaelt dann die juengsten 30_000. Loescht ausserdem
|
||||||
|
-- Eintraege aelter als 7 Tage. Verhindert die Endlosschleife aus 04/2026
|
||||||
|
-- (Bridge-EPIPE-Crash schrieb 5 Events/s und der Trigger scannte bei
|
||||||
|
-- jedem Insert die ganze Tabelle = O(n^2)-Schneeball).
|
||||||
|
DROP TRIGGER IF EXISTS cleanup_old_monitor_events;
|
||||||
CREATE TRIGGER IF NOT EXISTS cleanup_old_monitor_events
|
CREATE TRIGGER IF NOT EXISTS cleanup_old_monitor_events
|
||||||
AFTER INSERT ON monitor_events
|
AFTER INSERT ON monitor_events
|
||||||
|
WHEN (SELECT COUNT(*) FROM monitor_events) > 50000
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM monitor_events
|
DELETE FROM monitor_events
|
||||||
WHERE timestamp < datetime('now', '-7 days');
|
WHERE timestamp < datetime('now', '-7 days')
|
||||||
|
OR id IN (
|
||||||
|
SELECT id FROM monitor_events
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
LIMIT MAX((SELECT COUNT(*) FROM monitor_events) - 30000, 0)
|
||||||
|
);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
-- Projekte (für schnellen Wechsel)
|
-- Projekte (für schnellen Wechsel)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue