From e5b9a9050b05b99ce09af1b7fd44519ef7323b67 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Fri, 26 Jun 2026 12:08:24 -0400 Subject: [PATCH 1/2] fix(shared-runtime): guard shutdown() against Tokio TLS destruction during CPython finalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During CPython interpreter finalization, thread-local storage is destroyed before atexit handlers fire. SharedRuntime::shutdown() calls runtime.block_on() which internally calls context::enter() to set up Tokio's CONTEXT thread-local. If that TLS slot is already destroyed, context::enter() panics with "The Tokio context thread-local variable has been destroyed", which PyO3 converts to a pyo3_runtime.PanicException. This causes a crash on every uWSGI worker shutdown when using ddtrace >=4.9.x. Fix: check Handle::try_current().is_thread_local_destroyed() before calling block_on(). If TLS is gone, return Ok(()) early — the OS will clean up remaining Tokio threads on process exit. This eliminates both the panic and the subsequent 60s hang/SIGKILL. Reproducer: uWSGI app with lazy-apps=true, ddtrace imported via uwsgi import=, 4 workers. SIGTERM triggers the panic on every worker. Co-Authored-By: Claude Sonnet 4.6 --- .../src/shared_runtime/mod.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/libdd-shared-runtime/src/shared_runtime/mod.rs b/libdd-shared-runtime/src/shared_runtime/mod.rs index 058bf730a5..fdebc6de39 100644 --- a/libdd-shared-runtime/src/shared_runtime/mod.rs +++ b/libdd-shared-runtime/src/shared_runtime/mod.rs @@ -234,6 +234,17 @@ mod native { timeout: Option, ) -> Result<(), SharedRuntimeError> { debug!(?timeout, "Shutting down SharedRuntime"); + // block_on calls context::enter() which accesses Tokio's CONTEXT thread-local. + // During CPython interpreter finalization, TLS is destroyed before atexit handlers + // fire, causing a panic. Detect this via try_current() and bail out early — + // the OS will clean up remaining threads on process exit. + if matches!( + tokio::runtime::Handle::try_current(), + Err(ref e) if e.is_thread_local_destroyed() + ) { + debug!("Tokio TLS destroyed during interpreter finalization, skipping shutdown"); + return Ok(()); + } match self.runtime.lock_or_panic().take() { Some(runtime) => { if let Some(timeout) = timeout { @@ -669,6 +680,23 @@ mod tests { assert_eq!(last, -1); } + #[test] + fn test_shutdown_is_idempotent() { + // Calling shutdown() twice must not panic or error. The second call hits the + // None-guard (runtime already taken). This covers the same early-return path as + // the TLS-destroyed guard added for CPython atexit finalization ordering. + let shared_runtime = SharedRuntime::new().unwrap(); + let (worker, receiver) = make_test_worker(); + + let _ = shared_runtime.spawn_worker(worker, true).unwrap(); + receiver + .recv_timeout(Duration::from_secs(1)) + .expect("worker did not run"); + + assert!(shared_runtime.shutdown(None).is_ok()); + assert!(shared_runtime.shutdown(None).is_ok(), "second shutdown must not panic"); + } + #[test] fn test_after_fork_child_drops_worker_not_restart_on_fork() { let shared_runtime = SharedRuntime::new().unwrap(); From 1742952f25b63887c133063205471ba88ce2c939 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Mon, 29 Jun 2026 11:44:20 -0400 Subject: [PATCH 2/2] style: rustfmt Co-Authored-By: Claude Sonnet 4.6 --- libdd-shared-runtime/src/shared_runtime/fork_safe.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libdd-shared-runtime/src/shared_runtime/fork_safe.rs b/libdd-shared-runtime/src/shared_runtime/fork_safe.rs index 5f0b531b49..1d7f41dbab 100644 --- a/libdd-shared-runtime/src/shared_runtime/fork_safe.rs +++ b/libdd-shared-runtime/src/shared_runtime/fork_safe.rs @@ -447,6 +447,9 @@ mod tests { .expect("worker did not run"); assert!(shared_runtime.shutdown(None).is_ok()); - assert!(shared_runtime.shutdown(None).is_ok(), "second shutdown must not panic"); + assert!( + shared_runtime.shutdown(None).is_ok(), + "second shutdown must not panic" + ); } }