-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
GH-114781 potentially breaks gevent: threading becomes pre-imported at startup #117983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I note that zope is attempting to get away from pkg_resources-based namespace packages, which would bypass this issue, but only until another package came along that imported importlib.util early. It seems not unreasonable to me to defer the import of threading to facilitate gevent. |
Just a quick note that I had imported Trivially revertible, and it does still seem cleaner to me not to import threading unless lazy loading is actually used. |
Python doesn't support monkey patching on the standard library. This change has an impact on a core feature of Python, imports. I don't think that it's reasonable. gevent should adapt its monkey patching mecanism for latest importlib changes. |
Is it a negative impact on a core feature? Doesn't deferring the import of 'threading' actually improve import times of |
It can slow down exec_module(). Also, it's kind of weird that executing a module during an import will import indirectly another import :-( The threading module is special and is injected as part of the importlib bootstrap process. |
@jaraco, @effigies, @vstinner, thanks for feedback. @vstinner, I can understand your arguments, and that was the reason I was hesitant to open this issue in the first place. But also: here we are talking about Lines 236 to 272 in 17ed54b
From this point of view to me it is ok to say that the functionality, that only that Sure, gevent needs can be smashed very easily with the argument "python does not care about gevent monkey patching at all". If the level of required cooperation from the python side would be significant, that argument would indeed make sense, but here we are talking about insignificant amount of changes and it is really the attitude, not technical bits, which becomes decision driver. Even though "python way" is all asyncio this days, there are still many users of gevent out there. It would be good if the needs of those python+gevent users are also heard. Kirill |
P.S. I have no problem with running |
I might have suggested messing around with .pth files, but adding a new one won't necessarily help since they are loaded in the order they are identified by |
Threading support is only needed in multithreaded program. What if make |
I believe this example would fail if you did that: import lazy_loader as lazy # Assume `lazy_loader` does not import threading itself
np = lazy.load('numpy')
import threading
def trigger():
return np.__version__
def race():
for _ in range(10):
threading.Thread(target=trigger).start()
race() (The most recent version of The realistic scenario I see that would make conditionally using locks a problem would be a project that internally uses lazy loading to make its full API available with a single top-level import. If that gets imported before |
With another round of releases coming out, I feel like this needs resolution. I understand that monkey-patching the stdlib is unsupported, but this feels like a weird hill to die on, given that deferred imports are accepted practice elsewhere (e.g., #114664):
As the person who added the On the other hand, lazy loading is used when there is a performance gain to deferring the third-party module load, and slightly adding latency to the first lazy-load simply adjusts the calculation of whether a module is worth loading eagerly or lazily. If the difference of an extra I will open a PR that I hope will be backported, and I would ask the core developers to close this issue along with that PR if there is no intention of accepting a patch. |
As noted in gh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util.
…ythonGH-120233) As noted in pythongh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util. (cherry picked from commit 94f50f8) Co-authored-by: Chris Markiewicz <[email protected]>
…ythonGH-120233) As noted in pythongh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util. (cherry picked from commit 94f50f8) Co-authored-by: Chris Markiewicz <[email protected]>
…H-120233) (GH-121350) gh-117983: Defer import of threading for lazy module loading (GH-120233) As noted in gh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util. (cherry picked from commit 94f50f8) Co-authored-by: Chris Markiewicz <[email protected]>
…H-120233) (GH-121349) gh-117983: Defer import of threading for lazy module loading (GH-120233) As noted in gh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util. (cherry picked from commit 94f50f8) Co-authored-by: Chris Markiewicz <[email protected]>
All merged! |
And thanks to @effigies for staying on top of this! |
@brettcannon, thanks a lot for merging the fixes, and @effigies, thanks, once again, for improving things here. I see that the patch landed to 3.12, 3.13 and main, but, as original description of the issue explains, the bug was introduced in 3.11.10, so is there a possibility to backport to 3.11 as well? |
Probably not, as versions 3.11 and earlier receive security fixes only. |
@vsajip, thanks for feedback. I understand your reasoning, but it was a regression introduced in 3.11.10. From this point of view this patch is not a new addition, but reverts that regression back. In other words it does not add something new, but removes what was added in a recent point release and found to cause problems. For me it goes as common sense to apply the fix to 3.11.x as well, because else 3.11 leaves the whole gevent ecosystem in broken state. |
@pablogsal Would you consider this request to backport to 3.11 one more time in light of this additional information (that the downstream regression was introduced in a security-fix release)? (apologies if that was already a factor in your consideration) |
@jaraco the change was actually made while 3.11 was open to bugfixes: 46f821d was committed on Feb 26 and Python 3.11 entered security-only on April 2 w/ the 3.11.9 release. |
…ython#120233) As noted in pythongh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util.
…ython#120233) As noted in pythongh-117983, the import importlib.util can be triggered at interpreter startup under some circumstances, so adding threading makes it a potentially obligatory load. Lazy loading is not used in the stdlib, so this removes an unnecessary load for the majority of users and slightly increases the cost of the first lazily loaded module. An obligatory threading load breaks gevent, which monkeypatches the stdlib. Although unsupported, there doesn't seem to be an offsetting benefit to breaking their use case. For reference, here are benchmarks for the current main branch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 9.7 ms ± 0.7 ms [User: 7.7 ms, System: 1.8 ms] Range (min … max): 8.4 ms … 13.1 ms 313 runs ``` And with this patch: ``` ❯ hyperfine -w 8 './python -c "import importlib.util"' Benchmark 1: ./python -c "import importlib.util" Time (mean ± σ): 8.4 ms ± 0.7 ms [User: 6.8 ms, System: 1.4 ms] Range (min … max): 7.2 ms … 11.7 ms 352 runs ``` Compare to: ``` ❯ hyperfine -w 8 './python -c pass' Benchmark 1: ./python -c pass Time (mean ± σ): 7.6 ms ± 0.6 ms [User: 5.9 ms, System: 1.6 ms] Range (min … max): 6.7 ms … 11.3 ms 390 runs ``` This roughly halves the import time of importlib.util.
1. Upgrade testrunner image to the one generated by #10373 2. testrunner now runs python 3.11.9 (upgraded from 3.11.3) and gevent isn't compatible with python 3.11.9, python/cpython#117983, so update the test to run when version < 3.11.9 3. Regenerate riot file for appsec_integrations using python 3.12, as outdated pydantic doesn't work with 3.12.4, see pydantic/pydantic#9637. This also resulted in updating pytest version and `setup_module` is used instead of `setup`. 4. Add `CMAKE_BUILD_PARALLEL_LEVEL` to a few places ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
…g` has no attribute `Rlock` Summary: When using a recent version of python (3.12), running a sapling command returns the error `unknown python exception`: AttributeError: partially initialized module 'threading' has no attribute 'RLock' (most likely due to a circular import). This is caused by cpython breaking demandimport by `importing` threading locally in `importlib.util.LazyLoader.exec_module`. Adding `threading` along with `warnings`, and `_weakrefset` (which are imported by threading) to demandimport's ignore list resolves the issue. Refs: python/cpython#117983 https://repo.mercurial-scm.org/hg/file/63ede7a43a37/hgdemandimport/__init__.py https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076449 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076747
…g` has no attribute `Rlock` Summary: When using a recent version of python (3.12), running a sapling command returns the error `unknown python exception`: AttributeError: partially initialized module 'threading' has no attribute 'RLock' (most likely due to a circular import). This is caused by cpython breaking demandimport by importing `threading` locally in `importlib.util.LazyLoader.exec_module`. Adding `threading` along with `warnings`, and `_weakrefset` (which are imported by threading) to demandimport's ignore list resolves the issue. Refs: python/cpython#117983 https://repo.mercurial-scm.org/hg/file/63ede7a43a37/hgdemandimport/__init__.py https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076449 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076747
#977) Summary: …g` has no attribute `Rlock` When using a recent version of python (3.12), running a sapling command returns the error `unknown python exception`: AttributeError: partially initialized module 'threading' has no attribute 'RLock' (most likely due to a circular import). This is caused by cpython breaking demandimport by importing `threading` locally in `importlib.util.LazyLoader.exec_module`. Adding `threading` along with `warnings`, and `_weakrefset` (which are imported by threading) to demandimport's ignore list resolves the issue. Refs: python/cpython#117983 https://repo.mercurial-scm.org/hg/file/63ede7a43a37/hgdemandimport/__init__.py https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076449 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1076747 Pull Request resolved: #977 Reviewed By: ahornby Differential Revision: D65039760 fbshipit-source-id: 134258735005cb6710cb3f33e3c23eb3a000bcb6
Bug report
Bug description:
Hello up there.
I've discovered that starting from CPython >= 3.11.10 gevent-based applications
become potentially broken because, in a venv with gevent installed,
threading
module becomes pre-imported by python stdlib itself. As gevent injects its own
implementation of threading primitives, it helps to know that there are no
instances of C-implemented locks at the time of the monkey-patching: if there
are no such instances(*) the program is known to have only instances of
gevent-provided locks and it will not run into deadlock scenarios
described at e.g. gevent/gevent#1865 where the code
tries to take a lock, that lock turns to be standard thread lock, but there are
only greenlets running and there is no other real thread to release that lock.
So not having
threading
pre-imported at startup is very desirable property inthe context of gevent. For this reason gpython from pygolang actually verifies
that invariant and it is through which I've discovered the issue when testing
pygolang on py3.11.10:
The problem is there because
importlib.util
started to import threading after46f821d62b5a, and because setuptools
emits
importlib.util
usage in installed*-nspkg.pth
and in generated finders for packages installed in editable mode:https://github.com/pypa/setuptools/blob/92b45e9817ae829a5ca5a5962313a56b943cad91/setuptools/namespaces.py#L46-L61
https://github.com/pypa/setuptools/blob/92b45e98/setuptools/command/editable_wheel.py#L786-L790
So for example the following breaks because e.g. zope.event is installed via nspkg way:
So would you please consider applying the following suggested patch to fix this problem:
?
Thanks beforehand,
Kirill
/cc @effigies, @vstinner, @ericsnowcurrently, @doko42, @brettcannon
/cc @jamadden, @arcivanov, @maciejp-ro
/cc @eduardo-elizondo, @wilson3q, @vsajip
(*) or the list of C-implemented lock instances is limited and well defined -
for example gevent reinstantiates
thrading._active_limbo_lock
on themonkey-patching.
CPython versions tested on:
3.11, 3.12, CPython main branch
Operating systems tested on:
Linux
Linked PRs
The text was updated successfully, but these errors were encountered: