Skip to content

Commit 90ce960

Browse files
committed
improve background tests process cleanup
1 parent 8f820cd commit 90ce960

File tree

1 file changed

+29
-3
lines changed

1 file changed

+29
-3
lines changed

dash/testing/application_runners.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,28 @@ def __init__(self, **kwargs):
121121
self._old_threads = list(threading._active.keys()) # type: ignore[reportAttributeAccessIssue]; pylint: disable=W0212
122122

123123
def kill(self):
124+
# First, try to gracefully shut down ThreadPoolExecutor workers.
125+
# In Python 3.12+, Werkzeug's threaded mode uses ThreadPoolExecutor,
126+
# and we need to signal workers to exit cleanly before injecting SystemExit.
127+
# This prevents issues with Python's atexit handler trying to join killed threads.
128+
try:
129+
# pylint: disable=import-outside-toplevel
130+
from concurrent.futures import thread as _thread_module
131+
132+
if hasattr(_thread_module, "_threads_queues"):
133+
# Signal all thread pool workers to shut down
134+
# pylint: disable=protected-access
135+
for t, q in list(_thread_module._threads_queues.items()):
136+
if t.ident not in self._old_threads:
137+
try:
138+
q.put(None) # Shutdown sentinel
139+
except Exception: # pylint: disable=broad-except
140+
pass
141+
# Give workers a moment to exit cleanly
142+
time.sleep(0.1)
143+
except Exception: # pylint: disable=broad-except
144+
pass
145+
124146
# Kill all the new threads.
125147
for thread_id in list(threading._active): # type: ignore[reportAttributeAccessIssue]; pylint: disable=W0212
126148
if thread_id in self._old_threads:
@@ -130,7 +152,8 @@ def kill(self):
130152
ctypes.c_long(thread_id), ctypes.py_object(SystemExit)
131153
)
132154
if res == 0:
133-
raise ValueError(f"Invalid thread id: {thread_id}")
155+
# Thread might have already exited, which is fine
156+
continue
134157
if res > 1:
135158
ctypes.pythonapi.PyThreadState_SetAsyncExc(
136159
ctypes.c_long(thread_id), None
@@ -208,8 +231,11 @@ def run():
208231

209232
def stop(self):
210233
self.thread.kill() # type: ignore[reportOptionalMemberAccess]
211-
self.thread.join() # type: ignore[reportOptionalMemberAccess]
212-
wait.until_not(self.thread.is_alive, self.stop_timeout) # type: ignore[reportOptionalMemberAccess]
234+
# Give threads a moment to process the SystemExit
235+
time.sleep(0.1)
236+
self.thread.join(timeout=self.stop_timeout) # type: ignore[reportOptionalMemberAccess]
237+
# Don't use wait.until_not here - join() with timeout is sufficient
238+
# and avoids potential issues with thread state checking
213239
self.started = False
214240

215241

0 commit comments

Comments
 (0)