GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — test_concurrent_futures.test_shutdown: test_interpreter_shutdown() fails randomly (race condition) · Issue #109047 · python/cpython

test_concurrent_futures.test_shutdown: test_interpreter_shutdown() fails randomly (race condition) #109047

New issue
Copy link
New issue
Copy link
Closed
Closed
test_concurrent_futures.test_shutdown: test_interpreter_shutdown() fails randomly (race condition)#109047
Copy link
Labels
testsTests in the Lib/test dirTests in the Lib/test dir

Description

@vstinner
vstinner
opened on Sep 7, 2023
Issue body actions

The test_interpreter_shutdown() test of test_concurrent_futures.test_shutdown has a race condition. On purpose, the test doesn't wait until the executor completes (!). Moreover, it expects the executor to always be able to submit its job, and the job to complete successfully! It's a very optimistic bet.

See also issue #107219: test_concurrent_futures: test_crash_big_data() hangs randomly on Windows.


When Python is shutting down, Py_Finalize() quickly blocks the creation of new threads in _thread.start_new_thread():

cpython/Modules/_threadmodule.c

Lines 1161 to 1165 in 3bfa24e

if (interp->finalizing) {
PyErr_SetString(PyExc_RuntimeError,
"can't create new thread at interpreter shutdown");
return NULL;
}

This exception was added recently (last June) by commit ce558e6: see issue gh-104690 for the rationale.

The multiprocessing executor spawns _ExecutorManagerThread thread which runs its "main loop" in its run() method:

cpython/Lib/concurrent/futures/process.py

Lines 335 to 378 in 3bfa24e

def run(self):
# Main loop for the executor manager thread.
while True:
self.add_call_item_to_queue()
result_item, is_broken, cause = self.wait_result_broken_or_wakeup()
if is_broken:
self.terminate_broken(cause)
return
if result_item is not None:
self.process_result_item(result_item)
process_exited = result_item.exit_pid is not None
if process_exited:
p = self.processes.pop(result_item.exit_pid)
p.join()
# Delete reference to result_item to avoid keeping references
# while waiting on new results.
del result_item
if executor := self.executor_reference():
if process_exited:
with self.shutdown_lock:
executor._adjust_process_count()
else:
executor._idle_worker_semaphore.release()
del executor
if self.is_shutting_down():
self.flag_executor_shutting_down()
# When only canceled futures remain in pending_work_items, our
# next call to wait_result_broken_or_wakeup would hang forever.
# This makes sure we have some running futures or none at all.
self.add_call_item_to_queue()
# Since no new work items can be added, it is safe to shutdown
# this thread if there are no pending work items.
if not self.pending_work_items:
self.join_executor_internals()
return

It tries to submit new jobs to the worker process through a queue, but oops, the Python main thread is finalizing (called Py_Finalizing())! There is not notification system to notify threads that Python is being finalized.


Moreover, there are 3 "finalization" states:

  • interp->finalizing -- used by _thread.start_new_thread() to block thread creation during Python finazlization
  • runtime->_finalizing -- used by sys.is_finalizing(), Py_IsFinalizing() and _PyRuntimeState_GetFinalizing(runtime)
  • interp->_finalizing -- used by ceval.c to decide if a Python thread "must exit" or not, as soon as it's set, all Python threads must exit as soon as they attempt to acquire the GIL

These 3 states at not set at the same time.

  1. Calling Py_Finalize() sets interp->finalizing to 1 as soon as possible: so spawning new threads is immediately blocked (which is a good thing to get a reliable finalization!)
  2. Py_Finalize() calls threading._shutdown() which blocks until all non-daemon threads completes
  3. Py_Finalize() calls atexit callbacks
  4. And only then, Py_Finalize() sets runtime->_finalizing and interp->_finalizing to the Python thread state (tstate) which calls Py_Finalize()

The delay between (1) and (4) can be quite long, a thread can take several milliseconds, if not seconds, to complete.


Can multiprocessing or concurrent.futures check if Python is finalizing or be notified? Well, did you hear about Time-of-check to time-of-use race conditions? Even if it would be possible, I don't think that we can "check" if it's safe to spawn a thread just before spawning a thread, since the main thread can decide to finalize Python "at any time". It will become even more tricky with Python nogil ;-)

So what's left? Well, multiprocessing and concurrent.futures should be optimistic, call Python functions and only then check for exceptions. Depending on the exceptions, they can decide how to handle it. I would suggest to exit as soon as possible, and try to cleanup resources if possible.

Another option would be to make multiprocessing and concurrent.futures more determistic. Rather than spawning threads and processes in the background "on demand" and hope that everything will be fine, add more synchronization to "wait" until everything is ready to submit jobs. I think that I already tried this approach in the past, but @pitrou didn't like it since it made some workloads slower. You may not always need to actually submits jobs. You may not always need all threads and processes.

Well, I don't know even these complex modules to tell which option is the least bad :-)

Finally, as usually, I beg you to make these APIs less magical, and enforce more explicit resources management! It shouldn't even be possible to not wait until an executor complete. It should be enforced by emitting loudly ResourceWarning warnings :-) Well, that's my opinion. I know that it's not shared by @pitrou :-)

Linked PRs

  • gh-109047: Add PythonFinalizationError exception #109809
  • gh-109047: concurrent.futures catches RuntimeError #109810
  • [3.12] gh-109047: concurrent.futures catches RuntimeError (#109810) #110126

Activity

Next
  1. vstinner commented on Sep 7, 2023

    @vstinner
    vstinner
    on Sep 7, 2023
    MemberAuthor
    More actions

    This issue is made of multiple sub-issues.

    Here is a script to reproduce the RuntimeError: can't create new thread at interpreter shutdown error:

    from concurrent.futures import ProcessPoolExecutor
    from multiprocessing import get_context
    import os
    import atexit
    import faulthandler
    import random
    import time
    
    def sleep_and_print():
        delay = random.random() * 0.4
        print(f"sleep {delay*1e3:.1f} ms", flush=True)
        sleep(delay)
        print("apple", flush=True)
    
    def print_time(start_time):
        dt = time.perf_counter() - start_time
        print(f"exit in {dt*1e3:.1f} ms")
    
    def main():
        print(f"test pid={os.getpid()}")
        start_time = time.perf_counter()
        atexit.register(print_time, start_time)
    
        faulthandler.dump_traceback_later(5, exit=True)
        context = get_context('spawn')
        executor = ProcessPoolExecutor(5, mp_context=context)
        executor.submit(sleep_and_print)
        #faulthandler.cancel_dump_traceback_later()
        #executor.shutdown(wait=True)
        #print("exit")
    
    if __name__ == "__main__":
        main()

    The script is based on test_interpreter_shutdown() of Lib/test/test_concurrent_futures/test_shutdown.py.

    To trigger the issue, I run these two commands in parallel in two terminals:

    1. while true; do ./python script.py || break; done
    2. ./python -m test -j4 -r -- Increase -j4 if you cannot reproduce the issue. In fact, any command to stress t the system works.

    I easily reproduce the issue on FreeBSD. So far I failed reproduce the bug with this method on Linux.

    Logs:

    test pid=29139
    Exception in thread Thread-1:
    Traceback (most recent call last):
      File "/usr/home/vstinner/python/main/Lib/threading.py", line 1059, in _bootstrap_inner
        self.run()
      File "/usr/home/vstinner/python/main/Lib/concurrent/futures/process.py", line 339, in run
        self.add_call_item_to_queue()
      File "/usr/home/vstinner/python/main/Lib/concurrent/futures/process.py", line 394, in add_call_item_to_queue
        self.call_queue.put(_CallItem(work_id,
      File "/usr/home/vstinner/python/main/Lib/multiprocessing/queues.py", line 94, in put
        self._start_thread()
      File "/usr/home/vstinner/python/main/Lib/multiprocessing/queues.py", line 177, in _start_thread
        self._thread.start()
      File "/usr/home/vstinner/python/main/Lib/threading.py", line 978, in start
        _start_new_thread(self._bootstrap, ())
    RuntimeError: can't create new thread at interpreter shutdown
    exit in 122.7 ms
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/usr/home/vstinner/python/main/Lib/multiprocessing/spawn.py", line 122, in spawn_main
        exitcode = _main(fd, parent_sentinel)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/home/vstinner/python/main/Lib/multiprocessing/spawn.py", line 132, in _main
        self = reduction.pickle.load(from_parent)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/home/vstinner/python/main/Lib/multiprocessing/synchronize.py", line 115, in __setstate__
        self._semlock = _multiprocessing.SemLock._rebuild(*state)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    FileNotFoundError: [Errno 2] No such file or directory
    
  2. vstinner commented on Sep 7, 2023

    @vstinner
    vstinner
    on Sep 7, 2023
    MemberAuthor
    More actions

    I saw two kinds of behavior:

    • test_interpreter_shutdown() hangs on Windows
    • test_interpreter_shutdown() fails with RuntimeError: can't create new thread at interpreter shutdown on Linux on FreeBSD

    Errors:

    • August 25: Windows: Azure Pipelines: "Windows PR Tests win32"

      • test_interpreter_shutdown() hangs: killed by Timeout (0:20:00)!
      • test_concurrent_futures.test_deadlock: test_crash_big_data() hangs randomly on Windows #107219 (comment)
    • August 26: Linux: "aarch64 Fedora Stable LTO + PGO 3.x" buildbot

      • test_interpreter_shutdown() of ProcessPoolForkserverProcessPoolShutdownTest
      • test failed with: RuntimeError: can\'t create new thread at interpreter shutdown
      • test_concurrent_futures.test_deadlock: test_crash_big_data() hangs randomly on Windows #107219 (comment)
    • August 26: Linux: "ARM Raspbian 3.x" buildbot

      • test_interpreter_shutdown() of ProcessPoolForkserverProcessPoolShutdownTest
      • test failed with: RuntimeError: can't create new thread at interpreter shutdown
      • test_concurrent_futures.test_deadlock: test_crash_big_data() hangs randomly on Windows #107219 (comment)
  3. vstinner
    added
    testsTests in the Lib/test dirTests in the Lib/test dir
    on Sep 7, 2023
  4. vstinner commented on Sep 7, 2023

    @vstinner
    vstinner
    on Sep 7, 2023
    MemberAuthor
    More actions

    I confirm that the commit ce558e6 introduced the regression.

  5. gpshead commented on Sep 7, 2023

    @gpshead
    gpshead
    on Sep 7, 2023
    Member
    More actions

    I'm not surprised about the "cause" commit that exposed these problems. I agree with your overall analysis theme: We need to figure out how to make this situation more clear and handle the errors appropriately so that things that blindly thought they could keep running forever get a concrete exception that they can catch and cleanup and shutdown gracefully with. In some cases this probably also means adjusting the expectation of some tests.

    (I have noticed a few CI runs and buildbots with these errors popping up)

  6. serhiy-storchaka
    added this to Multiprocessing issueson Sep 10, 2023
  7. vstinner commented on Sep 22, 2023

    @vstinner
    vstinner
    on Sep 22, 2023
    MemberAuthor
    More actions

    Trying to check if Python is being finalized before running an action is unsafe, there is still a risk that Python finalization starts just after the check, and so the action (like creating a thread) will fail.

    The safe pattern is to raise an exception and catch the exception. My problem here is that RuntimeError is quite generic.

    @ericsnowcurrently @gpshead: What would you think of introducing a new PythonFinalizationError exception uses by the few spots which blocks actions during Python finalization? I'm open to other name suggestions :-)

    multiprocessing, threading and other modules could expect such error and cleans everything properly to handle Python finalization smoothly. For example, if a multiprocessing manager gets it, it would be a signal that ah, it's now time to stop everything.

    multiprocessing already has special code path for BrokenPipeError which is treated in similar way: see commit a9b1f84 which raises BrokenPipeError if a pipe is closed, so the caller gets BrokenPipeError and can exit properly. See gh-107219 and PR #109244.

    Reacted by Gregory P. Smith
  8. gpshead commented on Sep 23, 2023

    @gpshead
    gpshead
    on Sep 23, 2023
    Member
    More actions

    PythonFinalizationError is a good a name as any. This is the kind of exception we don't expect anyone (well, users anyways) to ever catch as there really isn't anything they can likely meaningfully do if the did. (like SystemError?)

  9. vstinner
    added 7 commits that reference this issue on Sep 24, 2023

    pythongh-109047: Add PythonFinalizationError exception

    f868ad7

    pythongh-109047: Add PythonFinalizationError exception

    f182fcd

    pythongh-109047: Add PythonFinalizationError exception

    6d969f3

    pythongh-109047: concurrent.futures _ExecutorManagerThread catches Py…

    3beed92

    pythongh-109047: concurrent.futures catches PythonFinalizationError

    bc555c5

    pythongh-109047: concurrent.futures catches PythonFinalizationError

    ab2a33b

    pythongh-109047: concurrent.futures catches PythonFinalizationError

    e286574
  10. 11 remaining items

  11. github-project-automation
    moved this to Done in Multiprocessing issueson Sep 29, 2023
  12. Yhg1s
    added a commit that references this issue on Oct 2, 2023

    [3.12] gh-109047: concurrent.futures catches RuntimeError (#109810) (#…

    356de02
  13. encukou
    added 2 commits that reference this issue on Jan 23, 2024

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    188228f

    [3.12] gh-114440: Close writer pipe in multiprocessing.Queue, not con…

    c912bc3
  14. naveen521kk
    added 5 commits that reference this issue on Feb 19, 2024

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    c4877f2

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    fd8463b

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    df2288a

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    df5f451

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    69198de
  15. naveen521kk
    added 3 commits that reference this issue on Jul 11, 2024

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    6a3e15b

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    23a82b7

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    701b465
  16. naveen521kk
    added a commit that references this issue on Aug 5, 2024

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    2ce1791
  17. Glyphack
    added a commit that references this issue on Sep 2, 2024

    pythongh-109047: concurrent.futures catches PythonFinalizationError (p…

    68b4f35
  18. naveen521kk
    added a commit that references this issue on Sep 4, 2024

    [3.12] pythongh-114440: Close writer pipe in multiprocessing.Queue, n…

    1ca1659
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    testsTests in the Lib/test dirTests in the Lib/test dir

    Projects

    • Multiprocessing issues
      Status
      Done
      Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.