Skip to content

Commit 4950e58

Browse files
fix(profiler): handle non-frame objects while stack-unwinding [backport 1.8] (#5091)
Backport ea7c9c6 from #5019 to 1.8. Following reports of Python 3.11 returning non-frame objects while unwinding the stack of a running thread, this change adds handling code that prevents exceptions from being raised. ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) are followed. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. Co-authored-by: Gabriele N. Tornetta <[email protected]>
1 parent fd6de8d commit 4950e58

File tree

3 files changed

+62
-25
lines changed

3 files changed

+62
-25
lines changed

ddtrace/profiling/collector/_traceback.pyx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
from types import CodeType
2+
from types import FrameType
3+
4+
from ddtrace.internal.logger import get_logger
5+
6+
7+
log = get_logger(__name__)
8+
9+
110
cpdef _extract_class_name(frame):
211
# type: (...) -> str
312
"""Extract class name from a frame, if possible.
@@ -47,13 +56,39 @@ cpdef pyframe_to_frames(frame, max_nframes):
4756
:param frame: The frame object to serialize.
4857
:param max_nframes: The maximum number of frames to return.
4958
:return: The serialized frames and the number of frames present in the original traceback."""
59+
# DEV: There are reports that Python 3.11 returns non-frame objects when
60+
# retrieving frame objects and doing stack unwinding. If we detect a
61+
# non-frame object we log a warning and return an empty stack, to avoid
62+
# reporting potentially incomplete and/or inaccurate data. This until we can
63+
# come to the bottom of the issue.
64+
if not isinstance(frame, FrameType):
65+
log.warning(
66+
"Got object of type '%s' instead of a frame object for the top frame of a thread", type(frame).__name__
67+
)
68+
return [], 0
69+
5070
frames = []
5171
nframes = 0
72+
5273
while frame is not None:
53-
nframes += 1
54-
if len(frames) < max_nframes:
74+
IF PY_MAJOR_VERSION > 3 or (PY_MAJOR_VERSION == 3 and PY_MINOR_VERSION >= 11):
75+
if not isinstance(frame, FrameType):
76+
log.warning(
77+
"Got object of type '%s' instead of a frame object during stack unwinding", type(frame).__name__
78+
)
79+
return [], 0
80+
81+
if nframes < max_nframes:
5582
code = frame.f_code
83+
IF PY_MAJOR_VERSION > 3 or (PY_MAJOR_VERSION == 3 and PY_MINOR_VERSION >= 11):
84+
if not isinstance(code, CodeType):
85+
log.warning(
86+
"Got object of type '%s' instead of a code object during stack unwinding", type(code).__name__
87+
)
88+
return [], 0
89+
5690
lineno = 0 if frame.f_lineno is None else frame.f_lineno
5791
frames.append((code.co_filename, lineno, code.co_name, _extract_class_name(frame)))
92+
nframes += 1
5893
frame = frame.f_back
5994
return frames, nframes

ddtrace/profiling/collector/stack.pyx

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -336,39 +336,37 @@ cdef stack_collect(ignore_profiler, thread_time, max_nframes, interval, wall_tim
336336
continue
337337

338338
frames, nframes = _traceback.pyframe_to_frames(task_pyframes, max_nframes)
339+
if nframes:
340+
stack_events.append(
341+
stack_event.StackSampleEvent(
342+
thread_id=thread_id,
343+
thread_native_id=thread_native_id,
344+
thread_name=thread_name,
345+
task_id=task_id,
346+
task_name=task_name,
347+
nframes=nframes, frames=frames,
348+
wall_time_ns=wall_time,
349+
sampling_period=int(interval * 1e9),
350+
)
351+
)
339352

353+
frames, nframes = _traceback.pyframe_to_frames(thread_pyframes, max_nframes)
354+
if nframes:
340355
event = stack_event.StackSampleEvent(
341356
thread_id=thread_id,
342357
thread_native_id=thread_native_id,
343358
thread_name=thread_name,
344-
task_id=task_id,
345-
task_name=task_name,
346-
nframes=nframes, frames=frames,
359+
task_id=thread_task_id,
360+
task_name=thread_task_name,
361+
nframes=nframes,
362+
frames=frames,
347363
wall_time_ns=wall_time,
364+
cpu_time_ns=cpu_time,
348365
sampling_period=int(interval * 1e9),
349366
)
350-
367+
event.set_trace_info(span, collect_endpoint)
351368
stack_events.append(event)
352369

353-
frames, nframes = _traceback.pyframe_to_frames(thread_pyframes, max_nframes)
354-
355-
event = stack_event.StackSampleEvent(
356-
thread_id=thread_id,
357-
thread_native_id=thread_native_id,
358-
thread_name=thread_name,
359-
task_id=thread_task_id,
360-
task_name=thread_task_name,
361-
nframes=nframes,
362-
frames=frames,
363-
wall_time_ns=wall_time,
364-
cpu_time_ns=cpu_time,
365-
sampling_period=int(interval * 1e9),
366-
)
367-
368-
event.set_trace_info(span, collect_endpoint)
369-
370-
stack_events.append(event)
371-
372370
if exception is not None:
373371
exc_type, exc_traceback = exception
374372
frames, nframes = _traceback.traceback_to_frames(exc_traceback, max_nframes)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
profiler: Handles potential ``AttributeErrors`` which would arise while collecting frames during stack unwinding in Python 3.11.

0 commit comments

Comments
 (0)