Skip to content

Commit e9ca96c

Browse files
committed
fix(di): dynamic function discovery fallback
We implement a dynamic function discovery fallback when the function-from-code resolution via the GC fails. This can happen if the target application has interacted with the GC, e.g. by freezing it at a time that will prevent the current discovery from being able to resolve the function from the referenced code object.
1 parent 9e78c42 commit e9ca96c

File tree

2 files changed

+40
-3
lines changed

2 files changed

+40
-3
lines changed

ddtrace/debugging/_function/discovery.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,42 @@ def at_line(self, line: int) -> List[FullyNamedFunction]:
326326

327327
return []
328328

329+
def _resolve_pair(self, pair: _FunctionCodePair, fullname: str) -> FullyNamedFunction:
330+
try:
331+
return pair.resolve()
332+
except ValueError:
333+
# We failed to lookup the function by its code object via the GC.
334+
# This should not happen, unless the target application is doing
335+
# something unusual with the GC. In this case we try our best to
336+
# look up the function dynamically from the module. If the function
337+
# is decorated, there are chances this might fail.
338+
target = (module := self._module)
339+
part = None
340+
for part in fullname.replace(f"{module.__name__}.", "", 1).split("."):
341+
try:
342+
target = getattr(target, part)
343+
except AttributeError:
344+
raise
345+
346+
if target is module:
347+
# We didn't move from the module so we don't have a function.
348+
raise
349+
350+
assert (code := pair.code) is not None
351+
f = undecorated(cast(FunctionType, target), cast(str, part), Path(code.co_filename).resolve())
352+
if not isinstance(target, FunctionType) and target.__code__ is not code:
353+
raise
354+
355+
# Cache the lookup result
356+
pair.function = f
357+
358+
return cast(FullyNamedFunction, f)
359+
329360
def by_name(self, qualname: str) -> FullyNamedFunction:
330361
"""Get the function by its qualified name."""
331362
fullname = f"{self._module.__name__}.{qualname}"
332363
try:
333-
return self._fullname_index[fullname].resolve()
364+
return self._resolve_pair(self._fullname_index[fullname], fullname)
334365
except ValueError:
335366
pass
336367
except KeyError:
@@ -342,11 +373,11 @@ def by_name(self, qualname: str) -> FullyNamedFunction:
342373
if qualname == name or qualname.endswith(f".{name}"):
343374
for fcp in list(fcps):
344375
try:
345-
f = fcp.resolve()
376+
f = self._resolve_pair(fcp, fullname)
346377

347378
# We have resolved the function so we can now
348379
# get its full name
349-
self._fullname_index[f"{self._module.__name__}.{f.__qualname__}"] = fcp
380+
self._fullname_index[fullname] = fcp
350381

351382
# We can remove the entry from the name index
352383
fcps.pop(0)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
dynamic instrumentation: improve support for function probes with frameworks
5+
and applications that interact with the Python garbage collector (e.g.
6+
synapse).

0 commit comments

Comments
 (0)