App crashes on restart: ggml Metal residency set assertion failure#897
Conversation
The `restart_app` command now performs the same cleanup sequence as the quit/close handlers before calling `app.restart()`: 1. Cancel ShutdownToken to signal background tasks (MCP, event forwarder) 2. Brief 50ms pause for tasks to exit their loops 3. Release Metal/GPU resources (LlamaState + LLAMA_BACKEND) 4. Then restart This prevents the SIGABRT caused by ggml_metal_rsets_free encountering active Metal residency sets during __cxa_finalize_ranges after std::process::exit(). Also addresses #885 (same crash on normal quit) since the quit path already had this fix — the restart path was simply missing it. Co-Authored-By: Claude <noreply@anthropic.com>
malibio
left a comment
There was a problem hiding this comment.
Pragmatic Code Review -- PR #897
Summary
Verdict: Approve (posted as comment due to self-review restriction). This is a clean, well-scoped bugfix that mirrors the existing graceful shutdown pattern (from the quit/close path in lib.rs) into the restart path. The change is minimal, correct, and directly addresses the SIGABRT crash caused by Metal residency sets still being active when std::process::exit() runs.
Requirements Validation (Issue #896)
| Acceptance Criterion | Status |
|---|---|
restart_app gracefully shuts down embedding engine before restarting |
Addressed |
| No SIGABRT / assertion failure on restart | Addressed |
| No crash report generated on restart | Follows from above |
| Normal quit (#885) also addressed if feasible | Already fixed in quit path; commit message notes this |
Findings
No Critical Issues
The implementation is sound. The cleanup sequence in restart_app exactly mirrors the proven pattern from CloseRequested/ExitRequested handlers in lib.rs (lines 426-438, 439-447):
- Cancel
ShutdownToken(signal background tasks) - 50ms
thread::sleep(let tasks exit loops) release_gpu_resources()(release Metal/GPU state)- Then proceed (restart or exit)
The visibility change from fn to pub(crate) fn on release_gpu_resources is the correct scope -- it stays internal to the crate while allowing cross-module access from commands/settings.rs.
[Improvement] Extract shared shutdown helper (DRY)
settings.rs:134-146 / lib.rs:433-437: The same three-step shutdown dance (cancel token + 50ms sleep + release GPU) is now duplicated across two call sites. Consider extracting into a shared helper:
// lib.rs
pub(crate) fn graceful_shutdown(app: &AppHandle, shutdown_token: &ShutdownToken) {
shutdown_token.cancel();
std::thread::sleep(std::time::Duration::from_millis(50));
release_gpu_resources(app);
}This would make future changes to the shutdown sequence (adjusting timeout, adding cleanup steps) less error-prone. However, given that issue #894 (AppServices container with tiered shutdown) is already the planned comprehensive fix, this is acceptable to defer.
[Nit] Redundant inner import
settings.rs:132: use tauri::Manager; inside the function body is redundant -- Manager is already imported at module scope on line 7 (use tauri::{AppHandle, Manager};). Removing the inner import would clean up the function slightly.
Positive Observations
- The commit message is excellent -- explains root cause, fix mechanics, and relationship to #885.
- The doc comment on
restart_appexplaining the "why" behind the cleanup is valuable for future maintainers. - The fix is appropriately scoped as a tactical patch; the strategic fix (#894) is tracked separately.
Reviewed by senior-architect-reviewer agent using the Pragmatic Quality framework.
…port - Extract `graceful_shutdown()` in lib.rs to DRY the shutdown sequence (cancel token → 50ms pause → release GPU) used by quit, close, and restart - CloseRequested and ExitRequested handlers now call graceful_shutdown() - restart_app simplified to graceful_shutdown() + app.restart() - Removed redundant `use tauri::Manager` inside restart_app (already at module scope) Addresses reviewer recommendations from PR #897 review. Co-Authored-By: Claude <noreply@anthropic.com>
Review Recommendations Addressed✅ Addressed (2/2)1. [Improvement] Extract shared shutdown helper (DRY)
2. [Nit] Redundant inner
⏭️ Skipped: 0Summary
🤖 Generated with Claude Code |
) * Fix: Graceful GPU shutdown before app restart (closes #896) The `restart_app` command now performs the same cleanup sequence as the quit/close handlers before calling `app.restart()`: 1. Cancel ShutdownToken to signal background tasks (MCP, event forwarder) 2. Brief 50ms pause for tasks to exit their loops 3. Release Metal/GPU resources (LlamaState + LLAMA_BACKEND) 4. Then restart This prevents the SIGABRT caused by ggml_metal_rsets_free encountering active Metal residency sets during __cxa_finalize_ranges after std::process::exit(). Also addresses #885 (same crash on normal quit) since the quit path already had this fix — the restart path was simply missing it. Co-Authored-By: Claude <noreply@anthropic.com> * Address review: Extract graceful_shutdown helper, remove redundant import - Extract `graceful_shutdown()` in lib.rs to DRY the shutdown sequence (cancel token → 50ms pause → release GPU) used by quit, close, and restart - CloseRequested and ExitRequested handlers now call graceful_shutdown() - restart_app simplified to graceful_shutdown() + app.restart() - Removed redundant `use tauri::Manager` inside restart_app (already at module scope) Addresses reviewer recommendations from PR #897 review. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Closes #896