Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tracing): avoid assigning span's local root to self, so that the python GC doesn't kick in #10809

Merged
merged 17 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions ddtrace/_trace/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class Span(object):
"duration_ns",
# Internal attributes
"_context",
"_local_root",
"_local_root_value",
"_parent",
"_ignored_exceptions",
"_on_finish_callbacks",
Expand Down Expand Up @@ -206,7 +206,7 @@ def __init__(
self._events: List[SpanEvent] = []
self._parent: Optional["Span"] = None
self._ignored_exceptions: Optional[List[Type[Exception]]] = None
self._local_root: Optional["Span"] = None
self._local_root_value: Optional["Span"] = None # None means this is the root span.
self._store: Optional[Dict[str, Any]] = None

def _ignore_exception(self, exc: Type[Exception]) -> None:
Expand Down Expand Up @@ -595,6 +595,23 @@ def context(self) -> Context:
self._context = Context(trace_id=self.trace_id, span_id=self.span_id, is_remote=False)
return self._context

@property
def _local_root(self) -> "Span":
if self._local_root_value is None:
return self
return self._local_root_value

@_local_root.setter
def _local_root(self, value: "Span") -> None:
if value is not self:
erikayasuda marked this conversation as resolved.
Show resolved Hide resolved
self._local_root_value = value
erikayasuda marked this conversation as resolved.
Show resolved Hide resolved
else:
self._local_root_value = None

@_local_root.deleter
def _local_root(self) -> None:
del self._local_root_value

def link_span(self, context: Context, attributes: Optional[Dict[str, Any]] = None) -> None:
"""Defines a causal relationship between two spans"""
if not context.trace_id or not context.span_id:
Expand Down
3 changes: 0 additions & 3 deletions ddtrace/_trace/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,8 +797,6 @@ def _start_span(
span._parent = parent
span._local_root = parent._local_root

if span._local_root is None:
span._local_root = span
for k, v in _get_metas_to_propagate(context):
# We do not want to propagate AppSec propagation headers
# to children spans, only across distributed spans
Expand All @@ -815,7 +813,6 @@ def _start_span(
span_api=span_api,
on_finish=[self._on_span_finish],
)
span._local_root = span
if config.report_hostname:
span.set_tag_str(HOSTNAME_KEY, hostname.get_hostname())

Expand Down
1 change: 1 addition & 0 deletions ddtrace/propagation/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,7 @@ def parent_call():
span_context = non_active_span.context

if hasattr(ddtrace, "tracer") and hasattr(ddtrace.tracer, "sample"):
erikayasuda marked this conversation as resolved.
Show resolved Hide resolved
root_span: Optional[Span] = None
erikayasuda marked this conversation as resolved.
Show resolved Hide resolved
if non_active_span is not None:
root_span = non_active_span._local_root
else:
Expand Down
4 changes: 4 additions & 0 deletions releasenotes/notes/fix-span-unnecessary-gc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
tracing: Removes a reference cycle that caused unnecessary garbage collection for top-level spans.
20 changes: 20 additions & 0 deletions tests/tracer/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2057,3 +2057,23 @@ def test_asm_standalone_configuration():
assert tracer._compute_stats is False
# reset tracer values
tracer.configure(appsec_enabled=False, appsec_standalone_enabled=False)


def test_gc_not_used_on_root_spans():
tracer = ddtrace.Tracer()
gc.freeze()

with tracer.trace("test-event"):
pass

# There should be no more span objects lingering around.
assert not any(str(obj).startswith("<Span") for obj in gc.get_objects())

# To check the exact nature of the objects and their references, use the following:

# for i, obj in enumerate(objects):
# print("--------------------")
# print(f"object {i}:", obj)
# print("referrers:", [f"object {objects.index(r)}" for r in gc.get_referrers(obj)[:-2]])
# print("referents:", [f"object {objects.index(r)}" if r in objects else r for r in gc.get_referents(obj)])
# print("--------------------")
erikayasuda marked this conversation as resolved.
Show resolved Hide resolved
Loading