@@ -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