Skip to content

Commit 4e2d633

Browse files
mergify[bot]brettlangdonKyle-Verhoog
authored
Fix memory leak when the tracer is disabled (backport #2964) (#2971)
The SpanAggregator will store spans in a list when on_span_start is called. When a span is finished on_span_finish is called and if the trace is complete the SpanAggregator will remove all of the traces spans from the list and give them to the writer. However, when the tracer is disabled it never calls on_span_finish so we never get an opportunity to remove a finished span from memory when it is finished because on_span_finish is never called. The fix here is to update the logic for on_span_start to only be called if the tracer is enabled. Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: Kyle Verhoog <[email protected]>
1 parent 7832583 commit 4e2d633

File tree

3 files changed

+46
-7
lines changed

3 files changed

+46
-7
lines changed

ddtrace/tracer.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -628,8 +628,10 @@ def _start_span(
628628
if service and service not in self._services and self._is_span_internal(span):
629629
self._services.add(service)
630630

631-
for p in self._span_processors:
632-
p.on_span_start(span)
631+
# Only call span processors if the tracer is enabled
632+
if self.enabled:
633+
for p in self._span_processors:
634+
p.on_span_start(span)
633635

634636
self._hooks.emit(self.__class__.start_span, span)
635637
return span
@@ -646,11 +648,10 @@ def _on_span_finish(self, span):
646648
"span %r closing after its parent %r, this is an error when not using async", span, span._parent
647649
)
648650

649-
if not self.enabled:
650-
return # nothing to do
651-
652-
for p in self._span_processors:
653-
p.on_span_finish(span)
651+
# Only call span processors if the tracer is enabled
652+
if self.enabled:
653+
for p in self._span_processors:
654+
p.on_span_finish(span)
654655

655656
if self.log.isEnabledFor(logging.DEBUG):
656657
self.log.debug("finishing span %s (enabled:%s)", span.pprint(), self.enabled)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
Fix memory leak caused when the tracer is disabled.

tests/tracer/test_tracer.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
tests for Tracer and utilities.
44
"""
55
import contextlib
6+
import gc
67
import multiprocessing
78
import os
89
from os import getpid
910
import threading
1011
from unittest.case import SkipTest
1112
import warnings
13+
import weakref
1214

1315
import mock
1416
import pytest
@@ -1693,3 +1695,35 @@ def test_fork_pid(tracer):
16931695
_, status = os.waitpid(pid, 0)
16941696
exit_code = os.WEXITSTATUS(status)
16951697
assert exit_code == 12
1698+
1699+
1700+
@pytest.mark.parametrize("enabled", [True, False])
1701+
def test_tracer_memory_leak_span_processors(enabled):
1702+
"""
1703+
Test whether the tracer or span processors will hold onto
1704+
span references after the trace is complete.
1705+
1706+
This is a regression test for the tracer not calling on_span_finish
1707+
of SpanAggregator when the tracer was disabled and traces leaking.
1708+
"""
1709+
spans = weakref.WeakSet()
1710+
1711+
# Filter to ensure we don't send the traces to the writer
1712+
class DropAllFilter:
1713+
def process_trace(self, trace):
1714+
return None
1715+
1716+
t = Tracer()
1717+
t.enabled = enabled
1718+
t.configure(settings={"FILTERS": [DropAllFilter()]})
1719+
1720+
for _ in range(5):
1721+
with t.trace("test") as span:
1722+
spans.add(span)
1723+
1724+
# Be sure to dereference the last Span held by the local variable `span`
1725+
span = None
1726+
1727+
# Force gc
1728+
gc.collect()
1729+
assert len(spans) == 0

0 commit comments

Comments
 (0)