Skip to content

Commit 7532d4a

Browse files
address Thomas' feedback
1 parent 1a31731 commit 7532d4a

4 files changed

Lines changed: 53 additions & 51 deletions

File tree

ddtrace/profiling/collector/_lock.pyx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# would cause Cython compilation errors since they aren't valid C types.
55

66
import _thread
7-
import logging
87
import os.path
98
import sys
109
import time
@@ -30,9 +29,10 @@ from ddtrace.profiling.collector._task cimport (
3029
get_task as _c_get_task,
3130
initialize_gevent_support as _c_initialize_gevent_support,
3231
)
32+
from ddtrace.internal.logger import get_logger
3333

3434

35-
LOG = logging.getLogger(__name__)
35+
log = get_logger(__name__)
3636

3737

3838
ACQUIRE_RELEASE_CO_NAMES: frozenset[str] = frozenset(["_acquire", "_release"])
@@ -122,7 +122,7 @@ class _ProfiledLock:
122122
# and should not generate profile samples to avoid double-counting
123123
self.is_internal: bool = is_internal
124124

125-
# gevent compatibility — gevent's _patch_existing_locks() calls
125+
# gevent compatibility — gevent's _patch_existing_locks calls
126126
# type(threading.RLock()) to determine the RLock type, then scans gc.get_objects()
127127
# for instances of that type. When our wrapper is on threading.RLock, the type is
128128
# _ProfiledLock, so ALL profiled lock instances match. gevent then checks each for
@@ -540,14 +540,16 @@ class LockCollector(collector.CaptureSamplerCollector):
540540
patched_module_id: int = id(self.MODULE)
541541
542542
def _on_module_reimport(new_module: ModuleType) -> None:
543-
nonlocal patched_module_id
543+
nonlocal patched_module_
544544
if id(new_module) == patched_module_id:
545545
return
546-
LOG.warning(
547-
"%s: target module %r was re-imported (id %#x -> %#x); "
548-
"re-applying lock profiling patches. "
549-
"This typically happens when gevent is installed and cleanup_loaded_modules() "
550-
"discards the previously-patched module from sys.modules.",
546+
log.warning(
547+
(
548+
"%s: target module %r was re-imported (id %#x -> %#x); "
549+
"re-applying lock profiling patches. "
550+
"This typically happens when gevent is installed and cleanup_loaded_modules() "
551+
"discards the previously-patched module from sys.modules."
552+
),
551553
type(self).__name__,
552554
module_name,
553555
patched_module_id,
@@ -568,17 +570,14 @@ class LockCollector(collector.CaptureSamplerCollector):
568570
super(LockCollector, self)._stop_service() # type: ignore[safe-super]
569571
self.unpatch()
570572
if self._reimport_hook is not None:
571-
try:
572-
ModuleWatchdog.unregister_module_hook(self.MODULE.__name__, self._reimport_hook)
573-
except Exception: # nosec B110
574-
pass
573+
ModuleWatchdog.unregister_module_hook(self.MODULE.__name__, self._reimport_hook)
575574
self._reimport_hook = None
576575
577576
def patch(self) -> None:
578577
"""Patch the module for tracking lock allocation."""
579578
original_lock: Callable[..., Any] = self._get_patch_target()
580579
if isinstance(original_lock, _LockAllocatorWrapper):
581-
LOG.debug(
580+
log.debug(
582581
"%s: %s.%s is already patched, skipping to avoid double-wrapping.",
583582
type(self).__name__,
584583
self.MODULE.__name__,

ddtrace/profiling/profiler.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,7 @@ def _stop_service(self, flush: bool = True, join: bool = True) -> None:
347347
# Prevent doing more initialisation now that we are shutting down.
348348
if self._collectors_on_import:
349349
for module, hook in self._collectors_on_import:
350-
try:
351-
ModuleWatchdog.unregister_module_hook(module, hook)
352-
except (ValueError, Exception):
353-
pass
350+
ModuleWatchdog.unregister_module_hook(module, hook)
354351
self._collectors_on_import = None
355352

356353
if self._scheduler is not None:

releasenotes/notes/fix-lock-profiler-module-cloning-4c524e7d7d5c5967.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,4 @@
22
fixes:
33
- |
44
profiling: Fixes an issue where the lock profiler silently stopped capturing
5-
lock events when running under ``ddtrace-run`` with gevent installed. The
6-
module cloning mechanism discarded the patched ``threading`` and ``asyncio``
7-
modules, causing user code to import fresh, unpatched modules. The lock
8-
profiler now re-applies patches automatically when target modules are
9-
re-imported.
5+
lock events when running under ``ddtrace-run`` with `gevent` installed.

tests/profiling/collector/test_threading.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ def test_patch():
194194
assert collector._original_lock == threading.Lock
195195

196196

197+
# Run in a subprocess: pops threading from sys.modules to simulate gevent's
198+
# cleanup_loaded_modules(), which would corrupt ModuleWatchdog state in-process.
199+
@pytest.mark.subprocess()
197200
def test_lock_patching_survives_module_reimport():
198201
"""Test that lock patches are re-applied when threading is re-imported.
199202
@@ -202,56 +205,63 @@ def test_lock_patching_survives_module_reimport():
202205
fresh, unpatched module. Without the fix, user code would get native locks.
203206
"""
204207
import importlib
208+
import sys
209+
import threading
210+
211+
from ddtrace.profiling.collector._lock import LockAllocatorWrapper
212+
from ddtrace.profiling.collector._lock import _ProfiledLock
213+
from ddtrace.profiling.collector.threading import ThreadingLockCollector
205214

206215
collector = ThreadingLockCollector()
207216
collector.start()
208217

209-
# Verify patching works on the current module
210218
assert isinstance(threading.Lock, LockAllocatorWrapper)
211219

212220
# Simulate cleanup_loaded_modules(): remove threading from sys.modules
213221
old_threading = sys.modules.pop("threading")
214-
try:
215-
# Re-import threading — this is what user code does after cloning
216-
new_threading = importlib.import_module("threading")
217-
assert new_threading is not old_threading, "Should be a different module object"
222+
# Re-import threading — this is what user code does after cloning
223+
new_threading = importlib.import_module("threading")
224+
assert new_threading is not old_threading, "Should be a different module object"
225+
226+
# The fix: patches should have been re-applied to the new module
227+
assert isinstance(new_threading.Lock, LockAllocatorWrapper), (
228+
"Lock on re-imported threading module should be patched. "
229+
"This fails without the ModuleWatchdog re-import hook fix."
230+
)
218231

219-
# The fix: patches should have been re-applied to the new module
220-
assert isinstance(new_threading.Lock, LockAllocatorWrapper), (
221-
"Lock on re-imported threading module should be patched. "
222-
"This fails without the ModuleWatchdog re-import hook fix."
223-
)
232+
# User-created locks from the new module should be profiled
233+
lock = new_threading.Lock()
234+
assert isinstance(lock, _ProfiledLock), "Locks created from re-imported threading should be profiled"
224235

225-
# User-created locks from the new module should be profiled
226-
lock = new_threading.Lock()
227-
assert isinstance(lock, _ProfiledLock), "Locks created from re-imported threading should be profiled"
228-
finally:
229-
# Restore original module to not break other tests
230-
sys.modules["threading"] = old_threading
231-
collector.stop()
236+
collector.stop()
232237

233238

239+
# Run in a subprocess: pops threading from sys.modules to simulate gevent's
240+
# cleanup_loaded_modules(), which would corrupt ModuleWatchdog state in-process.
241+
@pytest.mark.subprocess()
234242
def test_lock_unpatch_after_module_reimport():
235243
"""Test that stop/unpatch works correctly after module has been swapped."""
236244
import importlib
245+
import sys
246+
import threading
247+
248+
from ddtrace.profiling.collector._lock import LockAllocatorWrapper
249+
from ddtrace.profiling.collector.threading import ThreadingLockCollector
237250

238251
collector = ThreadingLockCollector()
239252
collector.start()
240253
assert isinstance(threading.Lock, LockAllocatorWrapper)
241254

242255
# Simulate module swap
243-
old_threading = sys.modules.pop("threading")
244-
try:
245-
new_threading = importlib.import_module("threading")
246-
assert isinstance(new_threading.Lock, LockAllocatorWrapper)
256+
sys.modules.pop("threading")
257+
new_threading = importlib.import_module("threading")
258+
assert isinstance(new_threading.Lock, LockAllocatorWrapper)
247259

248-
# Stop should unpatch the current (new) module
249-
collector.stop()
250-
assert not isinstance(new_threading.Lock, LockAllocatorWrapper), (
251-
"After stop, the new threading module should be unpatched"
252-
)
253-
finally:
254-
sys.modules["threading"] = old_threading
260+
# Stop should unpatch the current (new) module
261+
collector.stop()
262+
assert not isinstance(new_threading.Lock, LockAllocatorWrapper), (
263+
"After stop, the new threading module should be unpatched"
264+
)
255265

256266

257267
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)