55import sys
66
77
8- MODULES_LOADED_AT_STARTUP = frozenset (sys .modules .keys ())
9- MODULES_THAT_TRIGGER_CLEANUP_WHEN_INSTALLED = ("gevent" ,)
10-
11-
12- import os # noqa
13-
14-
15- """
16- The following modules cause problems when being unloaded/reloaded in module cloning.
17- Notably, unloading the atexit module will remove all registered hooks which we use for cleaning up on tracer shutdown.
18- The other listed modules internally maintain some state that does not coexist well if reloaded.
19- """
20- MODULES_TO_NOT_CLEANUP = {"atexit" , "asyncio" , "attr" , "concurrent" , "ddtrace" , "logging" }
21- if sys .version_info < (3 , 7 ):
22- MODULES_TO_NOT_CLEANUP |= {"typing" } # required by older versions of Python
23- if sys .version_info <= (2 , 7 ):
24- MODULES_TO_NOT_CLEANUP |= {"encodings" , "codecs" }
25- import imp
26-
27- _unloaded_modules = []
28-
29- def is_installed (module_name ):
30- try :
31- imp .find_module (module_name )
32- except ImportError :
33- return False
34- return True
35-
36-
37- else :
38- import importlib
39-
40- def is_installed (module_name ):
41- return importlib .util .find_spec (module_name )
42-
43-
44- def should_cleanup_loaded_modules ():
45- dd_unload_sitecustomize_modules = os .getenv ("DD_UNLOAD_MODULES_FROM_SITECUSTOMIZE" , default = "0" ).lower ()
46- if dd_unload_sitecustomize_modules not in ("1" , "auto" ):
47- return False
48- elif dd_unload_sitecustomize_modules == "auto" and not any (
49- is_installed (module_name ) for module_name in MODULES_THAT_TRIGGER_CLEANUP_WHEN_INSTALLED
50- ):
51- return False
52- return True
53-
54-
55- def cleanup_loaded_modules (aggressive = False ):
56- """
57- "Aggressive" here means "cleanup absolutely every module that has been loaded since startup".
58- Non-aggressive cleanup entails leaving untouched certain modules
59- This distinction is necessary because this function is used both to prepare for gevent monkeypatching
60- (requiring aggressive cleanup) and to implement "module cloning" (requiring non-aggressive cleanup)
61- """
62- # Figuring out modules_loaded_since_startup is necessary because sys.modules has more in it than just what's in
63- # import statements in this file, and unloading some of them can break the interpreter.
64- modules_loaded_since_startup = set (_ for _ in sys .modules if _ not in MODULES_LOADED_AT_STARTUP )
65- # Unload all the modules that we have imported, except for ddtrace and a few
66- # others that don't like being cloned.
67- # Doing so will allow ddtrace to continue using its local references to modules unpatched by
68- # gevent, while avoiding conflicts with user-application code potentially running
69- # `gevent.monkey.patch_all()` and thus gevent-patched versions of the same modules.
70- for module_name in modules_loaded_since_startup :
71- if aggressive :
72- del sys .modules [module_name ]
73- continue
74-
75- for module_to_not_cleanup in MODULES_TO_NOT_CLEANUP :
76- if module_name == module_to_not_cleanup :
77- break
78- elif module_name .startswith ("%s." % module_to_not_cleanup ):
79- break
80- else :
81- del sys .modules [module_name ]
82- # Some versions of CPython import the time module during interpreter startup, which needs to be unloaded.
83- if "time" in sys .modules :
84- del sys .modules ["time" ]
85-
86-
87- will_run_module_cloning = should_cleanup_loaded_modules ()
88- if not will_run_module_cloning :
89- # Perform gevent patching as early as possible in the application before
90- # importing more of the library internals.
91- if os .environ .get ("DD_GEVENT_PATCH_ALL" , "false" ).lower () in ("true" , "1" ):
92- # successfully running `gevent.monkey.patch_all()` this late into
93- # sitecustomize requires aggressive module unloading beforehand.
94- # gevent's documentation strongly warns against calling monkey.patch_all() anywhere other
95- # than the first line of the program. since that's what we're doing here,
96- # we cleanup aggressively beforehand to replicate the conditions at program start
97- # as closely as possible.
98- cleanup_loaded_modules (aggressive = True )
99- import gevent .monkey
100-
101- gevent .monkey .patch_all ()
8+ LOADED_MODULES = frozenset (sys .modules .keys ())
1029
10310import logging # noqa
10411import os # noqa
10512from typing import Any # noqa
10613from typing import Dict # noqa
14+ import warnings # noqa
10715
10816from ddtrace import config # noqa
10917from ddtrace .debugging ._config import config as debugger_config # noqa
18+ from ddtrace .internal .compat import PY2 # noqa
11019from ddtrace .internal .logger import get_logger # noqa
20+ from ddtrace .internal .module import find_loader # noqa
11121from ddtrace .internal .runtime .runtime_metrics import RuntimeWorker # noqa
11222from ddtrace .internal .utils .formats import asbool # noqa
11323from ddtrace .internal .utils .formats import parse_tags_str # noqa
@@ -142,6 +52,25 @@ def cleanup_loaded_modules(aggressive=False):
14252
14353log = get_logger (__name__ )
14454
55+ if os .environ .get ("DD_GEVENT_PATCH_ALL" ) is not None :
56+ deprecate (
57+ "The environment variable DD_GEVENT_PATCH_ALL is deprecated and will be removed in a future version. " ,
58+ postfix = "There is no special configuration necessary to make ddtrace work with gevent if using ddtrace-run. "
59+ "If not using ddtrace-run, import ddtrace.auto before calling gevent.monkey.patch_all()." ,
60+ removal_version = "2.0.0" ,
61+ )
62+ if "gevent" in sys .modules or "gevent.monkey" in sys .modules :
63+ import gevent .monkey # noqa
64+
65+ if gevent .monkey .is_module_patched ("threading" ):
66+ warnings .warn (
67+ "Loading ddtrace after gevent.monkey.patch_all() is not supported and is "
68+ "likely to break the application. Use ddtrace-run to fix this, or "
69+ "import ddtrace.auto before calling gevent.monkey.patch_all()." ,
70+ RuntimeWarning ,
71+ )
72+
73+
14574EXTRA_PATCHED_MODULES = {
14675 "bottle" : True ,
14776 "django" : True ,
@@ -162,6 +91,52 @@ def update_patched_modules():
16291 EXTRA_PATCHED_MODULES [module ] = asbool (should_patch )
16392
16493
94+ if PY2 :
95+ _unloaded_modules = []
96+
97+
98+ def is_module_installed (module_name ):
99+ return find_loader (module_name ) is not None
100+
101+
102+ def cleanup_loaded_modules ():
103+ MODULES_REQUIRING_CLEANUP = ("gevent" ,)
104+ do_cleanup = os .getenv ("DD_UNLOAD_MODULES_FROM_SITECUSTOMIZE" , default = "auto" ).lower ()
105+ if do_cleanup == "auto" :
106+ do_cleanup = any (is_module_installed (m ) for m in MODULES_REQUIRING_CLEANUP )
107+
108+ if not asbool (do_cleanup ):
109+ return
110+
111+ # Unload all the modules that we have imported, except for the ddtrace one.
112+ # NB: this means that every `import threading` anywhere in `ddtrace/` code
113+ # uses a copy of that module that is distinct from the copy that user code
114+ # gets when it does `import threading`. The same applies to every module
115+ # not in `KEEP_MODULES`.
116+ KEEP_MODULES = frozenset (["atexit" , "ddtrace" , "asyncio" , "concurrent" , "typing" , "logging" , "attr" ])
117+ for m in list (_ for _ in sys .modules if _ not in LOADED_MODULES ):
118+ if any (m == _ or m .startswith (_ + "." ) for _ in KEEP_MODULES ):
119+ continue
120+
121+ if PY2 :
122+ KEEP_MODULES_PY2 = frozenset (["encodings" , "codecs" ])
123+ if any (m == _ or m .startswith (_ + "." ) for _ in KEEP_MODULES_PY2 ):
124+ continue
125+ # Store a reference to deleted modules to avoid them being garbage
126+ # collected
127+ _unloaded_modules .append (sys .modules [m ])
128+
129+ del sys .modules [m ]
130+
131+ # TODO: The better strategy is to identify the core modues in LOADED_MODULES
132+ # that should not be unloaded, and then unload as much as possible.
133+ UNLOAD_MODULES = frozenset (["time" ])
134+ for u in UNLOAD_MODULES :
135+ for m in list (sys .modules ):
136+ if m == u or m .startswith (u + "." ):
137+ del sys .modules [m ]
138+
139+
165140try :
166141 from ddtrace import tracer
167142
@@ -202,19 +177,18 @@ def update_patched_modules():
202177 if not opts :
203178 tracer .configure (** opts )
204179
205- # We need to clean up after we have imported everything we need from
206- # ddtrace, but before we register the patch-on-import hooks for the
207- # integrations. This is because registering a hook for a module
208- # that is already imported causes the module to be patched immediately.
209- # So if we unload the module after registering hooks, we effectively
210- # remove the patching, thus breaking the tracer integration.
211- if will_run_module_cloning :
212- cleanup_loaded_modules ()
213180 if trace_enabled :
214181 update_patched_modules ()
215182 from ddtrace import patch_all
216183
184+ # We need to clean up after we have imported everything we need from
185+ # ddtrace, but before we register the patch-on-import hooks for the
186+ # integrations.
187+ cleanup_loaded_modules ()
188+
217189 patch_all (** EXTRA_PATCHED_MODULES )
190+ else :
191+ cleanup_loaded_modules ()
218192
219193 # Only the import of the original sitecustomize.py is allowed after this
220194 # point.
0 commit comments