Skip to content
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

fix(internal): remove nogevent compatibility layer #5105

Merged
merged 99 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
155ef9c
remove nogevent files
emmettbutler Feb 10, 2023
4903830
remove gevent compatibility code from periodic.py
emmettbutler Feb 10, 2023
9f95824
remove gevent compatibility code from a bunch of tests
emmettbutler Feb 10, 2023
a7192a7
remove gevent compatibility code from periodic tests
emmettbutler Feb 10, 2023
3a61a09
remove the import hook that reinitialized the gevent hub
emmettbutler Feb 10, 2023
59add29
remove nogevent compatibility layer from a few more tests
emmettbutler Feb 10, 2023
8430fcd
remove hanging references to DD_GEVENT_PATCH_ALL
emmettbutler Feb 10, 2023
8cfaacf
remove unused flag from test server
emmettbutler Feb 10, 2023
5ea55d4
pull out changes from #4863
emmettbutler Feb 10, 2023
a3411a8
do not patch gevent
emmettbutler Feb 10, 2023
348bd77
remove gevent check script
emmettbutler Feb 10, 2023
a770989
pull change from #4863
emmettbutler Feb 10, 2023
f41dfa9
move changes from #4863
emmettbutler Feb 10, 2023
b1a3d78
Flake8 fix
Yun-Kim Feb 15, 2023
6131217
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Feb 15, 2023
9df730f
fix a few missing subprocess imports
emmettbutler Feb 15, 2023
a0cea9e
fix a few missing subprocess imports
emmettbutler Feb 15, 2023
9b4eb5c
fix a few missing subprocess imports
emmettbutler Feb 15, 2023
2e8bad0
fix more tests
emmettbutler Feb 15, 2023
afa26f7
fix test
emmettbutler Feb 15, 2023
e89f0d4
unused import
emmettbutler Feb 15, 2023
a91ae87
Test all versions of Python for gunicorn test suite
Yun-Kim Feb 16, 2023
28d247d
Skip Python 2.7 tests for gunicorn
Yun-Kim Feb 16, 2023
1500f99
Merge branch 'emmett.butler/remove-gevent-hub-reinit-hack' into emmet…
emmettbutler Feb 16, 2023
008ad5e
merge branch emmett.butler/remove-gevent-patch-envvar
emmettbutler Feb 16, 2023
ceeef2c
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
juanjux Feb 17, 2023
411ea88
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
juanjux Feb 17, 2023
18531ff
simplify sitecustomize.py and use auto by default
P403n1x87 Feb 17, 2023
b0b959b
keep the logging module
P403n1x87 Feb 17, 2023
b4ec640
use find_loader to determine if module is installed
P403n1x87 Feb 17, 2023
e3227ff
Merge remote-tracking branch 'upstream/1.x' into emmett.butler/cleanu…
P403n1x87 Feb 17, 2023
3a1b9ae
count occurrences in gevent behaviour test
P403n1x87 Feb 17, 2023
ede7178
keep attr
P403n1x87 Feb 17, 2023
50898b2
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Feb 17, 2023
3d72af0
Update tests/profiling/collector/test_memalloc.py
emmettbutler Feb 21, 2023
390a683
Update tests/profiling/collector/test_memalloc.py
emmettbutler Feb 21, 2023
f67f3ad
Update tests/profiling/collector/test_memalloc.py
emmettbutler Feb 21, 2023
0e53317
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Feb 21, 2023
43e8b12
undo some apparently unnecessary deletions
emmettbutler Feb 21, 2023
918ccd9
release note
emmettbutler Feb 21, 2023
fadf3b0
change lineno expectation in test
emmettbutler Feb 21, 2023
0f12532
deprecation warning
emmettbutler Feb 21, 2023
b2c7e34
fix gevent version in py27 tests
emmettbutler Feb 21, 2023
3effb1c
typo
emmettbutler Feb 21, 2023
b7a3904
more general deprecation warning condition
emmettbutler Feb 21, 2023
017a38c
Remove check for profiler error to see errors in 3.5-3.7
Yun-Kim Feb 21, 2023
2245023
Enable DI on Python 3.11
Yun-Kim Feb 21, 2023
43ed7ec
flake8 isort for import
Yun-Kim Feb 21, 2023
f5c4359
run some profiler gevent tests in subprocesses with ddtrace-run
emmettbutler Feb 21, 2023
dfdf781
ignore task id when looking for main thread
emmettbutler Feb 21, 2023
4a11fe0
keep stdout connected to subprocess
emmettbutler Feb 21, 2023
83c114d
Always enable DI in post_fork, remove unnecessary gevent version
Yun-Kim Feb 21, 2023
8bfc4fe
simplify gunicorn tests
emmettbutler Feb 21, 2023
79e35ca
remove unused configurations from gunicorn tests
emmettbutler Feb 21, 2023
12c5cad
flake8
emmettbutler Feb 21, 2023
0bcfc9c
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Feb 21, 2023
dd215f2
remove unused code
emmettbutler Feb 22, 2023
9e7a3a9
use a simpler approach to run tests in ddtrace-run subprocess
emmettbutler Feb 22, 2023
c783724
Update ddtrace/bootstrap/sitecustomize.py
emmettbutler Feb 22, 2023
cdefc7e
clean documentation
emmettbutler Feb 22, 2023
f6908b6
fix deprecation syntax error
emmettbutler Feb 22, 2023
4dd7b20
guard memory collector iterator from race with _memalloc.stop
P403n1x87 Feb 22, 2023
d3a5eb3
adapt another preexisting test to work with gevent monkeypatching in …
emmettbutler Feb 22, 2023
e625805
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
Yun-Kim Feb 23, 2023
a08ca50
Remove unnecessary gevent test skips if gevent installed
Yun-Kim Feb 23, 2023
81569f1
give sitecustomize a nicer name that matches with existing patterns
emmettbutler Feb 23, 2023
ebeef33
include important note in gunicorn documentation
emmettbutler Feb 23, 2023
60142f5
Fix failing test caused by removing 3 lines
Yun-Kim Feb 23, 2023
4014ebf
re-add important comment
emmettbutler Feb 23, 2023
3053a94
Update releasenotes/notes/nogevent-6f2892cb412f987f.yaml
emmettbutler Feb 23, 2023
20929f2
big important developer note
emmettbutler Feb 23, 2023
7f29ee3
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Feb 24, 2023
1040f1c
add a warning about monkeypatching before sitecustomize
emmettbutler Feb 24, 2023
f13c794
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Feb 24, 2023
1147ec9
only import if gevent was already imported
emmettbutler Feb 24, 2023
ecc7d49
Update releasenotes/notes/nogevent-6f2892cb412f987f.yaml
emmettbutler Feb 27, 2023
5e0c1d2
Update releasenotes/notes/nogevent-6f2892cb412f987f.yaml
emmettbutler Feb 27, 2023
e074a97
Update ddtrace/bootstrap/sitecustomize.py
emmettbutler Feb 27, 2023
edb0aa9
Update ddtrace/contrib/gevent/__init__.py
emmettbutler Feb 27, 2023
ab84778
Update ddtrace/contrib/gunicorn/__init__.py
emmettbutler Feb 27, 2023
d7fd088
be a bit more direct in gunicorn ddtrace-run guidance
emmettbutler Feb 27, 2023
4c9a6bb
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Feb 27, 2023
2fcbef0
fix line length
emmettbutler Feb 27, 2023
3e2c935
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 1, 2023
8e046fb
fix sitecustomize.py formatting
P403n1x87 Mar 1, 2023
a8cd2e6
Merge remote-tracking branch 'upstream/1.x' into emmett.butler/cleanu…
P403n1x87 Mar 1, 2023
ede0533
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 1, 2023
8d005dd
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 1, 2023
921ee43
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 2, 2023
4dc15e2
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 2, 2023
97a4557
Update ddtrace/profiling/collector/_lock.py
emmettbutler Mar 2, 2023
3e11e13
Update releasenotes/notes/nogevent-6f2892cb412f987f.yaml
emmettbutler Mar 2, 2023
55e3ceb
Update releasenotes/notes/nogevent-6f2892cb412f987f.yaml
emmettbutler Mar 2, 2023
2aed200
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Mar 2, 2023
0f896f9
Update _lock.py
emmettbutler Mar 2, 2023
be8b534
Update stack.pyx
emmettbutler Mar 2, 2023
aaa5166
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 2, 2023
7ae585f
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
emmettbutler Mar 3, 2023
e345d0f
Merge branch '1.x' into emmett.butler/cleanups-before-removing-nogeve…
P403n1x87 Mar 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ddtrace/auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import ddtrace.bootstrap.sitecustomize # noqa
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
178 changes: 76 additions & 102 deletions ddtrace/bootstrap/sitecustomize.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,109 +5,19 @@
import sys


MODULES_LOADED_AT_STARTUP = frozenset(sys.modules.keys())
MODULES_THAT_TRIGGER_CLEANUP_WHEN_INSTALLED = ("gevent",)


import os # noqa


"""
The following modules cause problems when being unloaded/reloaded in module cloning.
Notably, unloading the atexit module will remove all registered hooks which we use for cleaning up on tracer shutdown.
The other listed modules internally maintain some state that does not coexist well if reloaded.
"""
MODULES_TO_NOT_CLEANUP = {"atexit", "asyncio", "attr", "concurrent", "ddtrace", "logging"}
if sys.version_info < (3, 7):
MODULES_TO_NOT_CLEANUP |= {"typing"} # required by older versions of Python
if sys.version_info <= (2, 7):
MODULES_TO_NOT_CLEANUP |= {"encodings", "codecs"}
import imp

_unloaded_modules = []

def is_installed(module_name):
try:
imp.find_module(module_name)
except ImportError:
return False
return True


else:
import importlib

def is_installed(module_name):
return importlib.util.find_spec(module_name)


def should_cleanup_loaded_modules():
dd_unload_sitecustomize_modules = os.getenv("DD_UNLOAD_MODULES_FROM_SITECUSTOMIZE", default="0").lower()
if dd_unload_sitecustomize_modules not in ("1", "auto"):
return False
elif dd_unload_sitecustomize_modules == "auto" and not any(
is_installed(module_name) for module_name in MODULES_THAT_TRIGGER_CLEANUP_WHEN_INSTALLED
):
return False
return True


def cleanup_loaded_modules(aggressive=False):
"""
"Aggressive" here means "cleanup absolutely every module that has been loaded since startup".
Non-aggressive cleanup entails leaving untouched certain modules
This distinction is necessary because this function is used both to prepare for gevent monkeypatching
(requiring aggressive cleanup) and to implement "module cloning" (requiring non-aggressive cleanup)
"""
# Figuring out modules_loaded_since_startup is necessary because sys.modules has more in it than just what's in
# import statements in this file, and unloading some of them can break the interpreter.
modules_loaded_since_startup = set(_ for _ in sys.modules if _ not in MODULES_LOADED_AT_STARTUP)
# Unload all the modules that we have imported, except for ddtrace and a few
# others that don't like being cloned.
# Doing so will allow ddtrace to continue using its local references to modules unpatched by
# gevent, while avoiding conflicts with user-application code potentially running
# `gevent.monkey.patch_all()` and thus gevent-patched versions of the same modules.
for module_name in modules_loaded_since_startup:
if aggressive:
del sys.modules[module_name]
continue

for module_to_not_cleanup in MODULES_TO_NOT_CLEANUP:
if module_name == module_to_not_cleanup:
break
elif module_name.startswith("%s." % module_to_not_cleanup):
break
else:
del sys.modules[module_name]
# Some versions of CPython import the time module during interpreter startup, which needs to be unloaded.
if "time" in sys.modules:
del sys.modules["time"]


will_run_module_cloning = should_cleanup_loaded_modules()
if not will_run_module_cloning:
# Perform gevent patching as early as possible in the application before
# importing more of the library internals.
if os.environ.get("DD_GEVENT_PATCH_ALL", "false").lower() in ("true", "1"):
# successfully running `gevent.monkey.patch_all()` this late into
# sitecustomize requires aggressive module unloading beforehand.
# gevent's documentation strongly warns against calling monkey.patch_all() anywhere other
# than the first line of the program. since that's what we're doing here,
# we cleanup aggressively beforehand to replicate the conditions at program start
# as closely as possible.
cleanup_loaded_modules(aggressive=True)
import gevent.monkey

gevent.monkey.patch_all()
LOADED_MODULES = frozenset(sys.modules.keys())

import logging # noqa
import os # noqa
from typing import Any # noqa
from typing import Dict # noqa
import warnings # noqa

from ddtrace import config # noqa
from ddtrace.debugging._config import config as debugger_config # noqa
from ddtrace.internal.compat import PY2 # noqa
from ddtrace.internal.logger import get_logger # noqa
from ddtrace.internal.module import find_loader # noqa
from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker # noqa
from ddtrace.internal.utils.formats import asbool # noqa
from ddtrace.internal.utils.formats import parse_tags_str # noqa
Expand Down Expand Up @@ -142,6 +52,25 @@ def cleanup_loaded_modules(aggressive=False):

log = get_logger(__name__)

if os.environ.get("DD_GEVENT_PATCH_ALL") is not None:
deprecate(
"The environment variable DD_GEVENT_PATCH_ALL is deprecated and will be removed in a future version. ",
postfix="There is no special configuration necessary to make ddtrace work with gevent if using ddtrace-run. "
"If not using ddtrace-run, import ddtrace.auto before calling gevent.monkey.patch_all().",
removal_version="2.0.0",
)
if "gevent" in sys.modules or "gevent.monkey" in sys.modules:
import gevent.monkey # noqa

if gevent.monkey.is_module_patched("threading"):
warnings.warn(
"Loading ddtrace after gevent.monkey.patch_all() is not supported and is "
"likely to break the application. Use ddtrace-run to fix this, or "
"import ddtrace.auto before calling gevent.monkey.patch_all().",
RuntimeWarning,
)


EXTRA_PATCHED_MODULES = {
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
"bottle": True,
"django": True,
Expand All @@ -162,6 +91,52 @@ def update_patched_modules():
EXTRA_PATCHED_MODULES[module] = asbool(should_patch)


if PY2:
_unloaded_modules = []


def is_module_installed(module_name):
return find_loader(module_name) is not None


def cleanup_loaded_modules():
MODULES_REQUIRING_CLEANUP = ("gevent",)
do_cleanup = os.getenv("DD_UNLOAD_MODULES_FROM_SITECUSTOMIZE", default="auto").lower()
if do_cleanup == "auto":
do_cleanup = any(is_module_installed(m) for m in MODULES_REQUIRING_CLEANUP)

if not asbool(do_cleanup):
return

# Unload all the modules that we have imported, except for the ddtrace one.
# NB: this means that every `import threading` anywhere in `ddtrace/` code
# uses a copy of that module that is distinct from the copy that user code
# gets when it does `import threading`. The same applies to every module
# not in `KEEP_MODULES`.
KEEP_MODULES = frozenset(["atexit", "ddtrace", "asyncio", "concurrent", "typing", "logging", "attr"])
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
for m in list(_ for _ in sys.modules if _ not in LOADED_MODULES):
if any(m == _ or m.startswith(_ + ".") for _ in KEEP_MODULES):
continue

if PY2:
KEEP_MODULES_PY2 = frozenset(["encodings", "codecs"])
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
if any(m == _ or m.startswith(_ + ".") for _ in KEEP_MODULES_PY2):
continue
# Store a reference to deleted modules to avoid them being garbage
# collected
_unloaded_modules.append(sys.modules[m])
Yun-Kim marked this conversation as resolved.
Show resolved Hide resolved

del sys.modules[m]

# TODO: The better strategy is to identify the core modues in LOADED_MODULES
# that should not be unloaded, and then unload as much as possible.
UNLOAD_MODULES = frozenset(["time"])
for u in UNLOAD_MODULES:
for m in list(sys.modules):
if m == u or m.startswith(u + "."):
del sys.modules[m]


try:
from ddtrace import tracer

Expand Down Expand Up @@ -202,19 +177,18 @@ def update_patched_modules():
if not opts:
tracer.configure(**opts)

# We need to clean up after we have imported everything we need from
# ddtrace, but before we register the patch-on-import hooks for the
# integrations. This is because registering a hook for a module
# that is already imported causes the module to be patched immediately.
# So if we unload the module after registering hooks, we effectively
# remove the patching, thus breaking the tracer integration.
if will_run_module_cloning:
cleanup_loaded_modules()
if trace_enabled:
update_patched_modules()
from ddtrace import patch_all

# We need to clean up after we have imported everything we need from
# ddtrace, but before we register the patch-on-import hooks for the
# integrations.
cleanup_loaded_modules()

patch_all(**EXTRA_PATCHED_MODULES)
else:
cleanup_loaded_modules()

# Only the import of the original sitecustomize.py is allowed after this
# point.
Expand Down
8 changes: 3 additions & 5 deletions ddtrace/contrib/gevent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
The integration patches the gevent internals to add context management logic.

.. note::
If :ref:`ddtrace-run<ddtracerun>` is being used set ``DD_GEVENT_PATCH_ALL=true`` and
``gevent.monkey.patch_all()`` will be called as early as possible in the application
to avoid patching conflicts.
If ``ddtrace-run`` is not being used then be sure to call ``gevent.monkey.patch_all``
before importing ``ddtrace`` and calling ``ddtrace.patch`` or ``ddtrace.patch_all``.
If ``ddtrace-run`` is not being used then be sure to ``import ddtrace.auto``
before calling ``gevent.monkey.patch_all``.
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
If ``ddtrace-run`` is being used then no additional configuration is required.


The integration also configures the global tracer instance to use a gevent
Expand Down
33 changes: 5 additions & 28 deletions ddtrace/contrib/gunicorn/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
"""
**Note:** ``ddtrace-run`` and Python 2 are both not supported with `Gunicorn <https://gunicorn.org>`__.
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
ddtrace works with Gunicorn.

``ddtrace`` only supports Gunicorn's ``gevent`` worker type when configured as follows:

- The application is running under a Python version >=3.6 and <=3.10
- `ddtrace-run` is not used
- The `DD_GEVENT_PATCH_ALL=1` environment variable is set
- Gunicorn's ```post_fork`` <https://docs.gunicorn.org/en/stable/settings.html#post-fork>`__ hook does not import from
``ddtrace``
- ``import ddtrace.bootstrap.sitecustomize`` is called either in the application's main process or in the
```post_worker_init`` <https://docs.gunicorn.org/en/stable/settings.html#post-worker-init>`__ hook.

.. code-block:: python

# gunicorn.conf.py
def post_fork(server, worker):
# don't touch ddtrace here
pass

def post_worker_init(worker):
import ddtrace.bootstrap.sitecustomize

workers = 4
worker_class = "gevent"
bind = "8080"

.. code-block:: bash

DD_GEVENT_PATCH_ALL=1 gunicorn --config gunicorn.conf.py path.to.my:app
.. note::
If you cannot wrap your Gunicorn server with the ``ddtrace-run``command and
emmettbutler marked this conversation as resolved.
Show resolved Hide resolved
it uses ``gevent`` workers, be sure to ``import ddtrace.auto`` as early as
possible in your application's lifecycle.
"""


Expand Down
22 changes: 0 additions & 22 deletions ddtrace/internal/forksafe.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import typing
import weakref

from ddtrace.internal.module import ModuleWatchdog
from ddtrace.internal.utils.formats import asbool
from ddtrace.vendor import wrapt


Expand All @@ -24,26 +22,6 @@
_soft = True


def patch_gevent_hub_reinit(module):
# The gevent hub is re-initialized *after* the after-in-child fork hooks are
# called, so we patch the gevent.hub.reinit function to ensure that the
# fork hooks run again after this further re-initialisation, if it is ever
# called.
from ddtrace.internal.wrapping import wrap

def wrapped_reinit(f, args, kwargs):
try:
return f(*args, **kwargs)
finally:
ddtrace_after_in_child()

wrap(module.reinit, wrapped_reinit)


if asbool(os.getenv("_DD_TRACE_GEVENT_HUB_PATCHED", default=False)):
ModuleWatchdog.register_module_hook("gevent.hub", patch_gevent_hub_reinit)


def ddtrace_after_in_child():
# type: () -> None
global _registry
Expand Down
Loading