bpo-33097: Fix submit accepting callable after executor shutdown by interpreter exit#6144
Conversation
|
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA). Unfortunately we couldn't find an account corresponding to your GitHub username on bugs.python.org (b.p.o) to verify you have signed the CLA (this might be simply due to a missing "GitHub Name" entry in your b.p.o account settings). This is necessary for legal reasons before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue. Thanks again to your contribution and we look forward to looking at it! |
a454482 to
60a1efa
Compare
60a1efa to
d7f7d45
Compare
…nterpreter exit. (pythonGH-6144) Executors in concurrent.futures accepted tasks after executor was shutdown by interpreter exit. Tasks were left in PENDING state forever. This fix changes submit to instead raise a RuntimeError.
d7f7d45 to
b1133c8
Compare
tomMoral
left a comment
There was a problem hiding this comment.
Thanks for spotting this! Here are a few comments on your patch.
This behavior is caused by the fact that when the interpreter is shutting down, the executor is not flagged as shutdown. For ProcessPoolExecutor, in L428, we do not set the _shutdown_thread flag as we consider that no job can be submitted after we detect that the executor is shutting down. We can thus set _shutdown_thread=True when the executor exists at this point to have a proper state in the executor.
The same inconsistency in the flag exists for ThreadPoolExecutor and fixing both would be a nice addition to this PR.
On the other hand, if the executor has not been started before the interpreter exit, a submit in atexit would launch processes and the cleaning process tends to be messy. So adding a check on global interpreter shutdown at submit avoids this messy state.
| @atexit.register | ||
| def run_last(): | ||
| try: | ||
| t.submit(lambda: None) |
There was a problem hiding this comment.
lambda expression are not picklable so this should be changed to id, None to avoid adding complexity.
| if self._broken: | ||
| raise BrokenProcessPool(self._broken) | ||
| if self._shutdown_thread: | ||
| if _global_shutdown or self._shutdown_thread: |
There was a problem hiding this comment.
Maybe add an explicit RuntimeError stating that the submit failed because the interpreter is shutting down.
There was a problem hiding this comment.
I changed this to an explicit RuntimeError in 881924f5bb37a38529ed2768d0227a5c528b45f0
| else: | ||
| from multiprocessing import get_context | ||
| context = get_context(context) | ||
| t = {executor_type}(5, mp_context=context) |
There was a problem hiding this comment.
Here, the executor is not started before the call in atexit. This causes another set of problem as the sub{Process,Thread} are not started and cleaned properly. For your proposed test, I would add a t.submit(id, 42).result() to be sure that the executor has been started before the atexit calls.
There was a problem hiding this comment.
Added t.submit(id, 42).result() to the test in d5c28194a0b842833ac4729f5f333ce718120201
|
For the record, this script causes a deadlock: import atexit
from multiprocessing import util
from multiprocessing import get_context
util.log_to_stderr(5)
executor = None
@atexit.register
def run_last():
global executor
if executor:
f = executor.submit(id, None)
util.debug("running future on executor flag {}\n\n\n".format(
executor._shutdown_thread))
f.result()
from concurrent.futures import ProcessPoolExecutor
if __name__ == "__main__":
context = get_context('spawn')
executor = ProcessPoolExecutor(5, mp_context=context)
executor.submit(id, 42).result() |
|
Hi @tomMoral, thanks for taking a look - I added changes as suggested. Regarding adding Do you think we should check it's not None and still set I am not sure how to fix this inconsistency in |
|
Great for the changes. For the if shutting_down():
try:
+ # Flag the executor as shutting down as early as possible if it is
+ # not gc-ed yet.
+ if executor is not None:
+ executor._shutdown_thread = True
# Since no new work items can be added, it is safe to shutdown
# this thread if there are no pending work items.
if not pending_work_items:
shutdown_worker()
return
except Full:
# This is not a problem: we will eventually be woken up (in
# result_queue.get()) and be able to send a sentinel again.
pass
executor = NoneFor the |
d5c2819 to
a1853a6
Compare
|
As suggested we now set Can you think of a way to test this behaviour or do you think that might not be necessary? |
| from concurrent.futures import {executor_type} | ||
| if __name__ == "__main__": | ||
| context = '{context}' | ||
| if context == "": |
There was a problem hiding this comment.
Nit: you can write if not context:.
| context=getattr(self, "ctx", ""))) | ||
| # Errors in atexit hooks don't change the process exit code, check | ||
| # stderr manually. | ||
| self.assertTrue(err) |
There was a problem hiding this comment.
Or you could write:
self.assertIn("RuntimeError: cannot schedule new futures", err.decode())
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
|
As a side note, I don't know whether we should backport this to 3.6, given the minor chance that it might cause incompatibilites with existing code (who knows?). 3.7 should be ok. |
|
Oh, and @mrknmc, could you sign the contributor's agreement as explained above? That will be required before merging your contribution :-) |
test_submit_after_interpreter_shutdown
|
Hi @pitrou, Thanks for looking over this! I made the suggested changes. I also signed the agreement again. I think maybe the first time I did it I didn't have my github username on bugs.python.org |
|
Thanks @mrknmc. I've pinged the PSF about the contributor agreement, perhaps it got lost somewhere. |
|
Ok, the PSF got your contributor agreement! |
|
GH-6445 is a backport of this pull request to the 3.7 branch. |
…nterpreter exit (pythonGH-6144) Executors in concurrent.futures accepted tasks after executor was shutdown by interpreter exit. Tasks were left in PENDING state forever. This fix changes submit to instead raise a RuntimeError. (cherry picked from commit c4b695f) Co-authored-by: Mark Nemec <[email protected]>
…nterpreter exit (GH-6144) (GH-6445) Executors in concurrent.futures accepted tasks after executor was shutdown by interpreter exit. Tasks were left in PENDING state forever. This fix changes submit to instead raise a RuntimeError. (cherry picked from commit c4b695f) Co-authored-by: Mark Nemec <[email protected]>
https://bugs.python.org/issue33097