Skip to content

Commit d64ee1c

Browse files
[mq] [skip ddci] working branch - merge 5e06537 on top of main at e6513ea
{"baseBranch":"main","baseCommit":"e6513eaaf42e37f4bbd194349846be01527586d3","createdAt":"2026-08-28T10:43:54.323937Z","headSha":"5e06537cee890d9d1c00166ab4769b118947aa56","id":"c1027454-7fef-44ed-b614-8e6d58946fc2","mergeMethod":"squash","priority":"200","pullRequestNumber":"19790","queuedAt":"2026-08-28T10:43:54.322731Z","status":"STATUS_QUEUED"}
2 parents f1c9760 + 5e06537 commit d64ee1c

2 files changed

Lines changed: 195 additions & 54 deletions

File tree

ddtrace/internal/module.py

Lines changed: 95 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,11 @@ def call_back(self, module: ModuleType) -> None:
230230
# loader type
231231
module.register_loader_type(_ImportHookChainedLoader, module.DefaultProvider)
232232

233-
for callback in self.callbacks.values():
234-
callback(module)
233+
for key, callback in self.callbacks.items():
234+
try:
235+
callback(module)
236+
except Exception:
237+
log.exception("Exception ignored in after_import hook %r for module %s", key, module.__name__)
235238

236239
def load_module(self, fullname: str) -> t.Optional[ModuleType]:
237240
if self.loader is None:
@@ -260,20 +263,10 @@ def _create_module(self, spec):
260263
def _find_first_hook(
261264
self, module: ModuleType, hooks_attr: str
262265
) -> t.Optional[t.Callable[[t.Any, ModuleType], None]]:
263-
for _ in sys.meta_path:
264-
if isinstance(_, ModuleWatchdog):
265-
try:
266-
for (
267-
cond,
268-
hook,
269-
) in getattr(_, hooks_attr, []):
270-
if (isinstance(cond, str) and cond == module.__name__) or (
271-
callable(cond) and cond(module.__name__)
272-
):
273-
return hook
274-
except Exception:
275-
log.debug("Exception happened while processing %s", hooks_attr, exc_info=True)
276-
return None
266+
universal = _UniversalModuleWatchdog._instance
267+
if universal is None:
268+
return None
269+
return universal._find_first_hook(module, hooks_attr)
277270

278271
def _find_first_exception_hook(self, module: ModuleType) -> t.Optional[t.Callable[[t.Any, ModuleType], None]]:
279272
return self._find_first_hook(module, "_import_exception_hooks")
@@ -330,15 +323,21 @@ def get_code(_loader, fullname):
330323
log.exception("Failed to call back on module %s", module)
331324

332325

333-
class BaseModuleWatchdog(abc.ABC):
334-
"""Base module watchdog.
326+
class _UniversalModuleWatchdog:
327+
"""The single finder ever inserted into ``sys.meta_path`` for module watchdog purposes.
335328
336-
Invokes ``after_import`` every time a new module is imported.
329+
Concrete ``BaseModuleWatchdog`` subclasses register their instances here as
330+
virtual participants, instead of each inserting itself into ``sys.meta_path``.
331+
This collapses what would otherwise be O(N) redundant, mutually-recursive
332+
``find_spec`` scans (one per installed watchdog, each re-scanning the rest of
333+
``sys.meta_path`` to find the real underlying finder) into a single O(1) lookup
334+
per import, regardless of how many watchdogs are installed.
337335
"""
338336

339-
_instance: t.Optional["BaseModuleWatchdog"] = None
337+
_instance: t.Optional["_UniversalModuleWatchdog"] = None
340338

341339
def __init__(self) -> None:
340+
self._watchdogs: list["BaseModuleWatchdog"] = []
342341
self._finding: set[str] = set()
343342

344343
# DEV: pkg_resources support to prevent errors such as
@@ -363,31 +362,14 @@ def __init__(self) -> None:
363362
else:
364363
log.warning("Cannot ensure correct support with pkg_resources")
365364

366-
def _add_to_meta_path(self) -> None:
367-
sys.meta_path.insert(0, self) # type: ignore[arg-type]
368-
369-
@classmethod
370-
def _find_in_meta_path(cls) -> t.Optional[int]:
371-
for i, meta_path in enumerate(sys.meta_path):
372-
if type(meta_path) is cls:
373-
return i
374-
return None
375-
376-
@classmethod
377-
def _remove_from_meta_path(cls) -> None:
378-
i = cls._find_in_meta_path()
379-
380-
if i is None:
381-
log.warning("%s is not installed", cls.__name__)
382-
return
383-
384-
sys.meta_path.pop(i)
365+
def register(self, participant: "BaseModuleWatchdog") -> None:
366+
self._watchdogs.append(participant)
385367

386-
def after_import(self, module: ModuleType) -> None:
387-
raise NotImplementedError()
388-
389-
def transform(self, code: CodeType, _module: ModuleType) -> CodeType:
390-
return code
368+
def unregister(self, participant: "BaseModuleWatchdog") -> None:
369+
try:
370+
self._watchdogs.remove(participant)
371+
except ValueError:
372+
pass
391373

392374
def find_module(self, fullname: str, path: t.Optional[str] = None) -> t.Optional["Loader"]:
393375
if fullname in self._finding:
@@ -404,8 +386,9 @@ def find_module(self, fullname: str, path: t.Optional[str] = None) -> t.Optional
404386
else original_loader
405387
)
406388

407-
loader.add_callback(type(self), self.after_import)
408-
loader.add_transformer(type(self), self.transform)
389+
for watchdog in list(self._watchdogs):
390+
loader.add_callback(type(watchdog), watchdog.after_import)
391+
loader.add_transformer(type(watchdog), watchdog.transform)
409392

410393
return t.cast("Loader", loader)
411394

@@ -452,21 +435,83 @@ def find_spec(
452435
if not isinstance(loader, _ImportHookChainedLoader):
453436
spec.loader = t.cast("Loader", _ImportHookChainedLoader(loader, spec))
454437

455-
t.cast(_ImportHookChainedLoader, spec.loader).add_callback(type(self), self.after_import)
456-
t.cast(_ImportHookChainedLoader, spec.loader).add_transformer(type(self), self.transform)
438+
for watchdog in list(self._watchdogs):
439+
t.cast(_ImportHookChainedLoader, spec.loader).add_callback(type(watchdog), watchdog.after_import)
440+
t.cast(_ImportHookChainedLoader, spec.loader).add_transformer(type(watchdog), watchdog.transform)
457441

458442
return spec
459443

460444
finally:
461445
self._finding.remove(fullname)
462446

447+
def _find_first_hook(
448+
self, module: ModuleType, hooks_attr: str
449+
) -> t.Optional[t.Callable[[t.Any, ModuleType], None]]:
450+
for watchdog in list(self._watchdogs):
451+
try:
452+
for (
453+
cond,
454+
hook,
455+
) in getattr(watchdog, hooks_attr, []):
456+
if (isinstance(cond, str) and cond == module.__name__) or (
457+
callable(cond) and cond(module.__name__)
458+
):
459+
return hook
460+
except Exception:
461+
log.debug("Exception happened while processing %s", hooks_attr, exc_info=True)
462+
return None
463+
464+
@classmethod
465+
def _register(cls, participant: "BaseModuleWatchdog") -> None:
466+
if cls._instance is None:
467+
cls._instance = cls()
468+
else:
469+
# DEV: Some other finder (eg: pytest's assertion rewriter) may have
470+
# inserted itself ahead of us in the meantime. Each individual watchdog
471+
# used to re-insert itself at position 0 on every install(), so match
472+
# that behavior by moving the shared instance back to the front on every
473+
# new registration, not just the very first one.
474+
try:
475+
sys.meta_path.remove(cls._instance) # type: ignore[arg-type]
476+
except ValueError:
477+
pass
478+
sys.meta_path.insert(0, cls._instance) # type: ignore[arg-type]
479+
cls._instance.register(participant)
480+
481+
@classmethod
482+
def _unregister(cls, participant: "BaseModuleWatchdog") -> None:
483+
if cls._instance is None:
484+
return
485+
cls._instance.unregister(participant)
486+
if not cls._instance._watchdogs:
487+
try:
488+
sys.meta_path.remove(cls._instance) # type: ignore[arg-type]
489+
except ValueError:
490+
log.warning("%s is not installed", cls.__name__)
491+
cls._instance = None
492+
493+
494+
class BaseModuleWatchdog(abc.ABC):
495+
"""Base module watchdog.
496+
497+
Invokes ``after_import`` every time a new module is imported.
498+
"""
499+
500+
_instance: t.Optional["BaseModuleWatchdog"] = None
501+
502+
def after_import(self, module: ModuleType) -> None:
503+
raise NotImplementedError()
504+
505+
def transform(self, code: CodeType, _module: ModuleType) -> CodeType:
506+
return code
507+
463508
@classmethod
464509
def install(cls) -> None:
465510
"""Install the module watchdog."""
466511
if cls.is_installed():
467512
return
468513
cls._instance = cls()
469-
cls._instance._add_to_meta_path()
514+
_UniversalModuleWatchdog._register(cls._instance)
470515
log.debug("%s installed", cls)
471516

472517
@classmethod
@@ -484,7 +529,7 @@ def uninstall(cls) -> None:
484529
if not cls.is_installed():
485530
return
486531

487-
cls._remove_from_meta_path()
532+
_UniversalModuleWatchdog._unregister(t.cast("BaseModuleWatchdog", cls._instance))
488533

489534
cls._instance = None
490535

tests/internal/test_module.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,24 @@ def test_watchdog_install_uninstall():
5656
import sys
5757

5858
from ddtrace.internal.module import ModuleWatchdog
59+
from ddtrace.internal.module import _UniversalModuleWatchdog
5960

6061
if ModuleWatchdog.is_installed():
6162
ModuleWatchdog.uninstall()
6263

6364
assert not ModuleWatchdog.is_installed()
64-
assert not any(isinstance(m, ModuleWatchdog) for m in sys.meta_path)
65+
assert not any(isinstance(m, _UniversalModuleWatchdog) for m in sys.meta_path)
6566

6667
ModuleWatchdog.install()
6768

6869
assert ModuleWatchdog.is_installed()
69-
assert isinstance(sys.meta_path[0], ModuleWatchdog)
70+
assert isinstance(sys.meta_path[0], _UniversalModuleWatchdog)
71+
assert ModuleWatchdog._instance in sys.meta_path[0]._watchdogs
7072

7173
ModuleWatchdog.uninstall()
7274

7375
assert not ModuleWatchdog.is_installed()
74-
assert not any(isinstance(m, ModuleWatchdog) for m in sys.meta_path)
76+
assert not any(isinstance(m, _UniversalModuleWatchdog) for m in sys.meta_path)
7577

7678

7779
def test_import_origin_hook_for_imported_module(module_watchdog):
@@ -616,6 +618,7 @@ def test_module_watchdog_find_spec_no_cross_thread_deadlock():
616618
import threading
617619

618620
from ddtrace.internal.module import ModuleWatchdog
621+
from ddtrace.internal.module import _UniversalModuleWatchdog
619622

620623
bootstrap = sys.modules.get("importlib._bootstrap")
621624
if bootstrap is None or not hasattr(bootstrap, "_get_module_lock"):
@@ -634,7 +637,7 @@ def test_module_watchdog_find_spec_no_cross_thread_deadlock():
634637

635638
def background():
636639
for finder in sys.meta_path:
637-
if isinstance(finder, ModuleWatchdog):
640+
if isinstance(finder, _UniversalModuleWatchdog):
638641
finder.find_spec("tests._ddtrace_regression_nonexistent", None, None)
639642
break
640643
background_done.set()
@@ -649,3 +652,96 @@ def background():
649652
)
650653
finally:
651654
lock.release()
655+
656+
657+
def test_universal_module_watchdog_single_finder_invariant():
658+
from ddtrace.internal.module import _UniversalModuleWatchdog
659+
660+
classes = [type(f"Watchdog{i}", (ModuleWatchdog,), {}) for i in range(5)]
661+
for cls in classes:
662+
cls.install()
663+
try:
664+
universal_finders = [m for m in sys.meta_path if isinstance(m, _UniversalModuleWatchdog)]
665+
assert len(universal_finders) == 1
666+
assert not any(cls._instance in sys.meta_path for cls in classes)
667+
finally:
668+
for cls in classes:
669+
cls.uninstall()
670+
671+
672+
def test_universal_module_watchdog_constant_find_spec_calls():
673+
from ddtrace.internal.module import _UniversalModuleWatchdog
674+
675+
# Ensure the parent package is already imported so that importing the
676+
# child module below triggers exactly one find_spec call, rather than one
677+
# per not-yet-imported ancestor package.
678+
import tests.submod.stuff # noqa:F401
679+
680+
for num_watchdogs in (1, 3, 6):
681+
classes = [type(f"Watchdog{i}", (ModuleWatchdog,), {}) for i in range(num_watchdogs)]
682+
for cls in classes:
683+
cls.install()
684+
try:
685+
universal = _UniversalModuleWatchdog._instance
686+
call_count = 0
687+
original_find_spec = universal.find_spec
688+
689+
def counting_find_spec(*args, **kwargs):
690+
nonlocal call_count
691+
call_count += 1
692+
return original_find_spec(*args, **kwargs)
693+
694+
universal.find_spec = counting_find_spec
695+
try:
696+
sys.modules.pop("tests.submod.stuff", None)
697+
import tests.submod.stuff # noqa:F401,F811
698+
finally:
699+
del universal.find_spec
700+
701+
assert call_count == 1, f"expected 1 find_spec call for {num_watchdogs} watchdogs, got {call_count}"
702+
finally:
703+
for cls in classes:
704+
cls.uninstall()
705+
sys.modules.pop("tests.submod.stuff", None)
706+
707+
708+
def test_universal_module_watchdog_first_registered_wins():
709+
calls = []
710+
711+
class First(ModuleWatchdog):
712+
pass
713+
714+
class Second(ModuleWatchdog):
715+
pass
716+
717+
First.install()
718+
Second.install()
719+
try:
720+
First.register_pre_exec_module_hook(
721+
lambda name: name == "tests.submod.stuff", lambda loader, module: calls.append("first")
722+
)
723+
Second.register_pre_exec_module_hook(
724+
lambda name: name == "tests.submod.stuff", lambda loader, module: calls.append("second")
725+
)
726+
727+
import tests.submod.stuff # noqa:F401
728+
729+
assert calls == ["first"]
730+
finally:
731+
sys.modules.pop("tests.submod.stuff", None)
732+
Second.uninstall()
733+
First.uninstall()
734+
735+
736+
def test_universal_module_watchdog_teardown():
737+
from ddtrace.internal.module import _UniversalModuleWatchdog
738+
739+
classes = [type(f"Watchdog{i}", (ModuleWatchdog,), {}) for i in range(3)]
740+
for cls in classes:
741+
cls.install()
742+
743+
for cls in classes:
744+
cls.uninstall()
745+
746+
assert _UniversalModuleWatchdog._instance is None
747+
assert not any(isinstance(m, _UniversalModuleWatchdog) for m in sys.meta_path)

0 commit comments

Comments
 (0)