Skip to content

Commit f65e636

Browse files
authored
Fix memory leak when the tracer is disabled (#2964)
* Fix memory leak when the tracer is disabled 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. * Add test case for memory leak * fix up test
1 parent ce25476 commit f65e636

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
@@ -630,8 +630,10 @@ def _start_span(
630630
if service and service not in self._services and self._is_span_internal(span):
631631
self._services.add(service)
632632

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

636638
self._hooks.emit(self.__class__.start_span, span)
637639
return span
@@ -648,11 +650,10 @@ def _on_span_finish(self, span):
648650
"span %r closing after its parent %r, this is an error when not using async", span, span._parent
649651
)
650652

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

657658
if self.log.isEnabledFor(logging.DEBUG):
658659
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
@@ -1706,3 +1708,35 @@ def test_tracer_api_version():
17061708

17071709
t.configure(api_version="v0.4")
17081710
assert isinstance(t.writer._encoder, MsgpackEncoderV03)
1711+
1712+
1713+
@pytest.mark.parametrize("enabled", [True, False])
1714+
def test_tracer_memory_leak_span_processors(enabled):
1715+
"""
1716+
Test whether the tracer or span processors will hold onto
1717+
span references after the trace is complete.
1718+
1719+
This is a regression test for the tracer not calling on_span_finish
1720+
of SpanAggregator when the tracer was disabled and traces leaking.
1721+
"""
1722+
spans = weakref.WeakSet()
1723+
1724+
# Filter to ensure we don't send the traces to the writer
1725+
class DropAllFilter:
1726+
def process_trace(self, trace):
1727+
return None
1728+
1729+
t = Tracer()
1730+
t.enabled = enabled
1731+
t.configure(settings={"FILTERS": [DropAllFilter()]})
1732+
1733+
for _ in range(5):
1734+
with t.trace("test") as span:
1735+
spans.add(span)
1736+
1737+
# Be sure to dereference the last Span held by the local variable `span`
1738+
span = None
1739+
1740+
# Force gc
1741+
gc.collect()
1742+
assert len(spans) == 0

0 commit comments

Comments
 (0)