Skip to content
6 changes: 5 additions & 1 deletion ddtrace/internal/_unpatched.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
# to get a reference to the right threading module.
import threading as _threading # noqa
import gc as _gc # noqa

import sys

threading_Lock = _threading.Lock
threading_RLock = _threading.RLock
threading_Event = _threading.Event


previous_loaded_modules = frozenset(sys.modules.keys())
from subprocess import Popen as unpatched_Popen # noqa # nosec B404
from os import close as unpatched_close # noqa: F401, E402
Expand Down
15 changes: 8 additions & 7 deletions ddtrace/internal/forksafe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import functools
import logging
import os
import threading
import typing
import weakref

import wrapt

from ddtrace.internal import _unpatched


log = logging.getLogger(__name__)

Expand Down Expand Up @@ -138,13 +139,13 @@ def _reset_object(self):
self.__wrapped__ = self._self_wrapped_class()


def Lock() -> threading.Lock:
return ResetObject(threading.Lock) # type: ignore
def Lock() -> _unpatched.threading_Lock:
return ResetObject(_unpatched.threading_Lock) # type: ignore


def RLock() -> threading.RLock:
return ResetObject(threading.RLock) # type: ignore
def RLock() -> _unpatched.threading_RLock:
return ResetObject(_unpatched.threading_RLock) # type: ignore


def Event() -> threading.Event:
return ResetObject(threading.Event) # type: ignore
def Event() -> _unpatched.threading_Event:
return ResetObject(_unpatched.threading_Event) # type: ignore
4 changes: 4 additions & 0 deletions releasenotes/notes/forksafe-lock-06ca188ef798c1f8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
core: This fix resolves an issue where forksafe locks used patched threading primitives from the profiling module, causing performance issues. The forksafe module now uses unpatched threading primitives (``Lock``, ``RLock``, ``Event``).
32 changes: 32 additions & 0 deletions tests/internal/test_forksafe.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections import Counter
import os
import sys

import pytest

Expand Down Expand Up @@ -204,6 +205,37 @@ def test_lock_fork():
assert exit_code == 12


@pytest.mark.skipif(sys.version_info >= (3, 14), reason="Profiling is not supported on Python 3.14 yet")
@pytest.mark.subprocess(
env=dict(DD_PROFILING_ENABLED="1"),
ddtrace_run=True,
)
def test_lock_unpatched():
"""Check that a forksafe.Lock is not patched when profiling is enabled."""

from ddtrace.internal import forksafe
from ddtrace.profiling import bootstrap
from ddtrace.profiling.collector.threading import ThreadingLockCollector

# When Profiler is started, bootstrap.profiler is set to the Profiler
# instance. We explicitly access the Profiler instance and the collector list
# to verify that the forksafe.Lock is not using the same class that is
# patched by ThreadingLockCollector that's running.
profiler = bootstrap.profiler._profiler
lock_collector = None
for c in profiler._collectors:
if isinstance(c, ThreadingLockCollector):
lock_collector = c
break

assert lock_collector is not None, "ThreadingLockCollector not found in profiler collectors"

lock = forksafe.Lock()
assert (
lock_collector._get_patch_target() is not lock._self_wrapped_class
), "forksafe.Lock is using the same class that is patched by ThreadingLockCollector"


def test_rlock_basic():
# type: (...) -> None
"""Check that a forksafe.RLock implements the correct threading.RLock interface"""
Expand Down
Loading