Skip to content

Commit 1467c02

Browse files
committed
fix wrapping context-lazy wrapping context compat
1 parent 45733c9 commit 1467c02

File tree

5 files changed

+70
-2
lines changed

5 files changed

+70
-2
lines changed

ddtrace/debugging/_function/discovery.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ def resolve(self) -> FullyNamedFunction:
139139
msg = "Cannot resolve pair with no code object"
140140
raise ValueError(msg)
141141

142-
if self.function is not None:
142+
# Check that the function we have cached is not a wrapper layer that
143+
# has been unwrapped. In this case we need to resolve the new function
144+
# from the code object.
145+
if (_ := self.function) is not None and _.__name__ != "<unwrapped>":
143146
return cast(FullyNamedFunction, self.function)
144147

145148
code = self.code

ddtrace/internal/wrapping/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,13 @@ def unwrap(wf, wrapper):
322322
# current one.
323323
f = cast(FunctionType, wf)
324324
f.__code__ = inner.__code__
325+
326+
# Mark the function as unwrapped via its name. There might be references
327+
# to this function elsewhere and this would signal that the function has
328+
# been unwrapped and that another function object is referencing the
329+
# original code object.
330+
inner.__name__ = "<unwrapped>"
331+
325332
try:
326333
# Update the link to the next layer.
327334
inner_wf = cast(WrappedFunction, inner)

ddtrace/internal/wrapping/context.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,29 @@ def wrap(self) -> None:
394394
if self._trampoline is not None:
395395
return
396396

397+
# If the function is already universally wrapped so it's less expensive
398+
# to do the normal wrapping.
399+
if _UniversalWrappingContext.is_wrapped(self.__wrapped__):
400+
super().wrap()
401+
return
402+
397403
def trampoline(_, args, kwargs):
398404
with tl:
399405
f = t.cast(WrappedFunction, self.__wrapped__)
400406
if is_wrapped_with(self.__wrapped__, trampoline):
407+
# If the wrapped function was instrumented with a
408+
# wrapping context before the first invocation we need
409+
# to carry that over to the original function when we
410+
# remove the trampoline.
411+
try:
412+
inner = f.__dd_wrapped__
413+
except AttributeError:
414+
inner = None
401415
f = unwrap(f, trampoline)
416+
try:
417+
f.__dd_context_wrapped__ = inner.__dd_context_wrapped__
418+
except AttributeError:
419+
pass
402420
super(LazyWrappingContext, self).wrap()
403421
return f(*args, **kwargs)
404422

tests/debugging/mocking.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def assert_single_snapshot(self):
177177

178178
assert len(self.test_queue) == 1
179179

180-
yield self.test_queue[0]
180+
yield self.test_queue.pop(0)
181181

182182

183183
@contextmanager

tests/debugging/test_debugger.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,46 @@ def test_debugger_line_probe_on_lazy_wrapped_function(stuff):
642642
assert snapshot.probe.probe_id == "line-probe-lazy-wrapping"
643643

644644

645+
def test_debugger_function_probe_on_lazy_wrapped_function(stuff):
646+
from ddtrace.internal.wrapping.context import LazyWrappingContext
647+
648+
class LWC(LazyWrappingContext):
649+
entered = False
650+
651+
def __enter__(self):
652+
self.entered = True
653+
return super().__enter__()
654+
655+
(c := LWC(stuff.throwexcstuff)).wrap()
656+
657+
probe = create_snapshot_function_probe(
658+
probe_id="function-probe-lazy-wrapping",
659+
module="tests.submod.stuff",
660+
func_qname="throwexcstuff",
661+
rate=float("inf"),
662+
)
663+
664+
with debugger() as d:
665+
# Test that we can re-instrument the function correctly and that we
666+
# don't accidentally instrument the temporary trampoline instead.
667+
for _ in range(10):
668+
d.add_probes(probe)
669+
670+
try:
671+
stuff.throwexcstuff()
672+
except Exception:
673+
pass
674+
675+
d.remove_probes(probe)
676+
677+
assert c.entered
678+
679+
c.entered = False
680+
681+
with d.assert_single_snapshot() as snapshot:
682+
assert snapshot.probe.probe_id == "function-probe-lazy-wrapping"
683+
684+
645685
def test_probe_status_logging(remote_config_worker, stuff):
646686
assert remoteconfig_poller.status == ServiceStatus.STOPPED
647687

0 commit comments

Comments
 (0)