mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 09:47:44 +02:00
[eric] crash: shutdown fuse hard-exits a wedged backend tree in 10s, uvicorn drains connections on an 8s budget, and three GPU deaths buy one boot of software rendering with an honest pill
This commit is contained in:
@@ -215,6 +215,9 @@ async def service_lifespan():
|
||||
|
||||
yield
|
||||
|
||||
# Graceful shutdown just began; if it wedges (the ENG-223 quit race), the fuse hard-exits the tree.
|
||||
from backend.apps.service.shutdown_fuse import arm_shutdown_fuse
|
||||
arm_shutdown_fuse()
|
||||
if p_pulse_task:
|
||||
p_pulse_task.cancel()
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""SIGTERM arms a hard exit fuse (ENG-223): the quit path can wedge mid-shutdown (measured live: a
|
||||
quit racing a pending update left uvicorn hung 8+ minutes with its whole agent-CLI tree orphaned at
|
||||
~700MB), so if graceful shutdown has not finished FUSE_S after TERM, the fuse kills our process
|
||||
tree and exits. A daemon thread, so a wedged event loop cannot block it."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
from typeguard import typechecked
|
||||
|
||||
FUSE_S = 10.0
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_descendant_pids() -> List[int]:
|
||||
pids: List[int] = []
|
||||
frontier: List[int] = [os.getpid()]
|
||||
for depth in range(6):
|
||||
next_frontier: List[int] = []
|
||||
for parent in frontier:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["pgrep", "-P", str(parent)], capture_output=True, text=True, timeout=2,
|
||||
).stdout
|
||||
except Exception:
|
||||
continue
|
||||
for tok in out.split():
|
||||
try:
|
||||
next_frontier.append(int(tok))
|
||||
except ValueError:
|
||||
pass
|
||||
pids.extend(next_frontier)
|
||||
if not next_frontier:
|
||||
break
|
||||
frontier = next_frontier
|
||||
return pids
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_burn() -> None:
|
||||
for pid in p_descendant_pids():
|
||||
try:
|
||||
os.kill(pid, 9)
|
||||
except Exception:
|
||||
pass
|
||||
os._exit(0)
|
||||
|
||||
|
||||
@typechecked
|
||||
def arm_shutdown_fuse() -> None:
|
||||
"""Called at lifespan-shutdown START (already past TERM), so no signal handling: just the timer. Touching signal.signal here would clobber uvicorn's asyncio-installed handlers."""
|
||||
if os.name == "nt":
|
||||
return
|
||||
timer = threading.Timer(FUSE_S, p_burn)
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
+19
-2
@@ -113,11 +113,16 @@ process.on('unhandledRejection', (reason) => {
|
||||
});
|
||||
|
||||
// child-process-gone fires for GPU/utility/renderer process deaths. The GPU one is especially useful: a GPU crash forces the renderer to recover its compositor, and that recovery can itself crash on Windows.
|
||||
let gpuCrashCount = 0;
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
console.error('[diag][main:child-process-gone]', JSON.stringify(details));
|
||||
// Clean exits and user kills are not crashes; reporting them would bury the real ones.
|
||||
if (details && details.reason && details.reason !== 'clean-exit' && details.reason !== 'killed') {
|
||||
crashReports.writeCrashReport('child-process-gone', details);
|
||||
// Three GPU deaths in one session: the compositor is losing on this machine, so the NEXT boot runs software rendering (one boot only; the marker is consumed at startup). ENG-228.
|
||||
if (details.type === 'GPU' && ++gpuCrashCount >= 3) {
|
||||
try { fs.writeFileSync(GPU_FALLBACK_MARKER, String(Date.now())); } catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Platform-split auto-updater: electron-updater on Mac (full-featured), Electron's
|
||||
@@ -602,6 +607,18 @@ async function startFrontendServer() {
|
||||
});
|
||||
}
|
||||
|
||||
const GPU_FALLBACK_MARKER = path.join(os.homedir(), 'Library', 'Application Support', 'openswarm', 'gpu-fallback.marker');
|
||||
let reducedGraphicsThisBoot = false;
|
||||
// Must run BEFORE app ready: a marker from last session's repeated GPU crashes buys ONE boot of software rendering, then normal service resumes (the marker is consumed here). ENG-228.
|
||||
try {
|
||||
if (process.platform === 'darwin' && fs.existsSync(GPU_FALLBACK_MARKER)) {
|
||||
fs.unlinkSync(GPU_FALLBACK_MARKER);
|
||||
app.disableHardwareAcceleration();
|
||||
reducedGraphicsThisBoot = true;
|
||||
console.warn('[gpu-fallback] repeated GPU crashes last session; this boot uses software rendering');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const isPackaged = app.isPackaged;
|
||||
const isDev = process.env.ELECTRON_DEV === '1';
|
||||
|
||||
@@ -700,7 +717,7 @@ function detectDirtyExitAndArmSafeMode() {
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('get-safe-mode', () => safeModeInfo);
|
||||
ipcMain.handle('get-safe-mode', () => ({ ...safeModeInfo, reducedGraphics: reducedGraphicsThisBoot }));
|
||||
|
||||
// Quit-cause forensics. On a real quit (Cmd+Q, dock Quit, app.quit()) Electron
|
||||
// fires before-quit BEFORE any window 'close' events; a window closing on its
|
||||
@@ -1201,7 +1218,7 @@ async function startBackend() {
|
||||
|
||||
backendProcess = spawn(
|
||||
pythonPath,
|
||||
['-m', 'uvicorn', 'backend.main:app', '--host', '127.0.0.1', '--port', String(backendPort)],
|
||||
['-m', 'uvicorn', 'backend.main:app', '--host', '127.0.0.1', '--port', String(backendPort), '--timeout-graceful-shutdown', '8'],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
env,
|
||||
|
||||
@@ -12,7 +12,7 @@ const SafeModePill: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const info = safeModeInfo();
|
||||
if (!info.safeMode || dismissed) return null;
|
||||
if ((!info.safeMode && !info.reducedGraphics) || dismissed) return null;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -26,7 +26,9 @@ const SafeModePill: React.FC = () => {
|
||||
>
|
||||
<ShieldOutlinedIcon sx={{ fontSize: 16, color: c.text.tertiary }} />
|
||||
<Typography sx={{ fontSize: '0.8125rem', color: c.text.primary }}>
|
||||
Recovered after repeated crashes. Browsers and apps are paused; click one to resume it.
|
||||
{info.safeMode
|
||||
? 'Recovered after repeated crashes. Browsers and apps are paused; click one to resume it.'
|
||||
: 'Running in reduced graphics mode after repeated graphics crashes. Restart to return to full speed.'}
|
||||
</Typography>
|
||||
<Box
|
||||
role="button" aria-label="Dismiss safe mode notice" onClick={() => setDismissed(true)}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface SafeModeInfo {
|
||||
safeMode: boolean;
|
||||
dirtyCount: number;
|
||||
fingerprint: { exception: string | null; code: number | null; address: number | null } | null;
|
||||
reducedGraphics?: boolean;
|
||||
}
|
||||
|
||||
let cached: SafeModeInfo = { safeMode: false, dirtyCount: 0, fingerprint: null };
|
||||
|
||||
Reference in New Issue
Block a user