Shut down Jupyter kernels when their kernel scope ends (bd-hxhnnlzs) - #491
Merged
Conversation
Every process that executed a Jupyter document — the test suite and q2 render alike — leaked one ipykernel process per (kernel, dir): sessions live in a process-global static, statics never drop, so the Child's kill_on_drop never fired and kernels reparented to PID 1 (2338 accumulated on one dev machine, each holding ~6 sockets). Fix: refcounted KernelScope. The jupyter engine's execute_qmd holds an inner scope around each engine run, so no caller can leak; outer scopes in q2 render (around pipeline.run(), dropped before any process::exit), q2 preview and q2 provide-hub (server lifetime) keep kernels warm across documents/re-renders. When the last scope drops, all sessions shut down: shutdown_request on the control channel (when a private runtime is possible), kill backstop, reap, and connection-file removal. Two adjacent latent bugs surfaced by the new e2e test, both fixed: - get_or_start_session TOCTOU: concurrent renders sharing a session key both spawned kernels (read-check -> multi-second spawn -> insert; the loser was evicted and killed). Every two-doc project render hit it. Now serialized behind a start_lock with a re-check. - Cross-call session reuse never actually worked: sessions held ZeroMQ sockets and the kernel Child bound to a per-call throwaway runtime, so genuine reuse died with "Tokio context ... being shutdown" (masked by the TOCTOU race always re-spawning). Sessions now live on a shared long-lived ENGINE_RUNTIME. Also deleted the never-called idle-timeout machinery (cleanup_idle_sessions, with_idle_timeout, last_used/touch), and added panic-safety scopes to the direct-daemon-API tests. Tests (written first, verified failing pre-fix): - quarto-core: jupyter_kernel_cleanup.rs drives record_capture under an isolated JUPYTER_RUNTIME_DIR and asserts kernel ports are dead and connection files gone when it returns. - quarto: jupyter_kernel_cleanup_e2e.rs drives the real q2 binary on a two-python-doc project; asserts exactly one kernel served both docs (pinning warm reuse) and nothing survives process exit. The two #[ignore]d jupyter_integration pipeline tests fail on main with a pre-existing nested-runtime panic — filed as bd-yaccefzk. Plan: claude-notes/plans/2026-08-10-jupyter-kernel-leak.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the Jupyter kernel process leak (bd-hxhnnlzs): every process that executed a Jupyter document — the test suite and
q2 renderalike — leaked one ipykernel process per (kernel, working dir). 2338 orphans had accumulated on one dev machine, each reparented to PID 1 and holding ~6 listening sockets, plus 4932 stalekernel-*.jsonconnection files.Root cause: kernel sessions live in a process-global
static DAEMON, statics never drop, so the kernelChild'skill_on_dropnever fired and nothing ever calledshutdown_all()(the idle reaper had zero callers).Fix — refcounted
KernelScope:execute_qmdholds an inner scope around each engine run, so no caller can leak by accident.q2 render(aroundpipeline.run(), dropped before anyprocess::exit),q2 preview, andq2 provide-hub(server lifetime) keep kernels warm across documents / re-renders.shutdown_requeston the control channel (when a private runtime is possible), SIGKILL backstop, bounded reap, connection-file removal.Two adjacent latent bugs surfaced by the new e2e test, both fixed:
start_lockwith a re-check.Childbound to a per-call throwaway tokio runtime, so genuine reuse failed with "Tokio context … being shutdown" (masked by the TOCTOU race always re-spawning). Sessions now live on a shared long-livedENGINE_RUNTIME.Also deletes the never-called idle-timeout machinery and adds panic-safety scopes to the direct-daemon-API tests.
Tests (TDD — written first, verified failing pre-fix)
quarto-corejupyter_kernel_cleanup.rs: drivesrecord_captureunder an isolatedJUPYTER_RUNTIME_DIR, records each spawned kernel's ports from its connection file, asserts ports are dead + files removed when it returns.quartojupyter_kernel_cleanup_e2e.rs: drives the realq2binary on a two-python-doc website; asserts exactly one kernel served both docs (pinning warm reuse) and nothing survives process exit.Verification
cargo nextest run --workspace: 11273/11273 passed, zero orphaned kernels after the run (previously ~15 per run).cargo xtask verify(incl. WASM leg) andcargo xtask lint: green.cargo run --bin q2 -- render <two-python-doc website>→ "Rendered 2 of 2 files", executed outputs present in both HTML files, zero orphan processes, zero stale connection files (output inspected).#[ignore]djupyter_integrationpipeline tests fail on main with a pre-existing nested-runtime panic — filed as bd-yaccefzk, not a regression from this change.Plan/findings:
claude-notes/plans/2026-08-10-jupyter-kernel-leak.md🤖 Generated with Claude Code