Skip to content

Commit c509eac

Browse files
authored
Merge pull request #795 from DataDog/0.20.1-dev
0.20.1
2 parents 05d8402 + 02149d0 commit c509eac

File tree

5 files changed

+37
-20
lines changed

5 files changed

+37
-20
lines changed

ddtrace/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .tracer import Tracer
55
from .settings import config
66

7-
__version__ = '0.20.0'
7+
__version__ = '0.20.1'
88

99
# a global tracer instance with integration settings
1010
tracer = Tracer()

ddtrace/contrib/celery/signals.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def trace_before_publish(*args, **kwargs):
8888
# Note: adding tags from `traceback` or `state` calls will make an
8989
# API call to the backend for the properties so we should rely
9090
# only on the given `Context`
91-
attach_span(task, task_id, span)
91+
attach_span(task, task_id, span, is_publish=True)
9292

9393

9494
def trace_after_publish(*args, **kwargs):
@@ -102,12 +102,12 @@ def trace_after_publish(*args, **kwargs):
102102
return
103103

104104
# retrieve and finish the Span
105-
span = retrieve_span(task, task_id)
105+
span = retrieve_span(task, task_id, is_publish=True)
106106
if span is None:
107107
return
108108
else:
109109
span.finish()
110-
detach_span(task, task_id)
110+
detach_span(task, task_id, is_publish=True)
111111

112112

113113
def trace_failure(*args, **kwargs):

ddtrace/contrib/celery/utils.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,40 +39,55 @@ def tags_from_context(context):
3939
return tags
4040

4141

42-
def attach_span(task, task_id, span):
42+
def attach_span(task, task_id, span, is_publish=False):
4343
"""Helper to propagate a `Span` for the given `Task` instance. This
4444
function uses a `WeakValueDictionary` that stores a Datadog Span using
45-
the `task_id` as a key. This is useful when information must be
45+
the `(task_id, is_publish)` as a key. This is useful when information must be
4646
propagated from one Celery signal to another.
47+
48+
DEV: We use (task_id, is_publish) for the key to ensure that publishing a
49+
task from within another task does not cause any conflicts.
50+
51+
This mostly happens when either a task fails and a retry policy is in place,
52+
or when a task is manually retries (e.g. `task.retry()`), we end up trying
53+
to publish a task with the same id as the task currently running.
54+
55+
Previously publishing the new task would overwrite the existing `celery.run` span
56+
in the `weak_dict` causing that span to be forgotten and never finished.
57+
58+
NOTE: We cannot test for this well yet, because we do not run a celery worker,
59+
and cannot run `task.apply_async()`
4760
"""
4861
weak_dict = getattr(task, CTX_KEY, None)
4962
if weak_dict is None:
5063
weak_dict = WeakValueDictionary()
5164
setattr(task, CTX_KEY, weak_dict)
5265

53-
weak_dict[task_id] = span
66+
weak_dict[(task_id, is_publish)] = span
5467

5568

56-
def detach_span(task, task_id):
69+
def detach_span(task, task_id, is_publish=False):
5770
"""Helper to remove a `Span` in a Celery task when it's propagated.
5871
This function handles tasks where the `Span` is not attached.
5972
"""
6073
weak_dict = getattr(task, CTX_KEY, None)
6174
if weak_dict is None:
6275
return
6376

64-
weak_dict.pop(task_id, None)
77+
# DEV: See note in `attach_span` for key info
78+
weak_dict.pop((task_id, is_publish), None)
6579

6680

67-
def retrieve_span(task, task_id):
81+
def retrieve_span(task, task_id, is_publish=False):
6882
"""Helper to retrieve an active `Span` stored in a `Task`
6983
instance
7084
"""
7185
weak_dict = getattr(task, CTX_KEY, None)
7286
if weak_dict is None:
7387
return
7488
else:
75-
return weak_dict.get(task_id)
89+
# DEV: See note in `attach_span` for key info
90+
return weak_dict.get((task_id, is_publish))
7691

7792

7893
def retrieve_task_id(context):

tests/contrib/celery/base.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
1-
import unittest
2-
31
from celery import Celery
42

53
from ddtrace import Pin, config
64
from ddtrace.compat import PY2
75
from ddtrace.contrib.celery import patch, unpatch
86

97
from ..config import REDIS_CONFIG
10-
from ...test_tracer import get_dummy_tracer
8+
from ...base import BaseTracerTestCase
119

1210

1311
REDIS_URL = 'redis://127.0.0.1:{port}'.format(port=REDIS_CONFIG['port'])
1412
BROKER_URL = '{redis}/{db}'.format(redis=REDIS_URL, db=0)
1513
BACKEND_URL = '{redis}/{db}'.format(redis=REDIS_URL, db=1)
1614

1715

18-
class CeleryBaseTestCase(unittest.TestCase):
16+
class CeleryBaseTestCase(BaseTracerTestCase):
1917
"""Test case that handles a full fledged Celery application with a
2018
custom tracer. It patches the new Celery application.
2119
"""
2220

2321
def setUp(self):
22+
super(CeleryBaseTestCase, self).setUp()
23+
2424
# keep track of original config
2525
self._config = dict(config.celery)
2626
# instrument Celery and create an app with Broker and Result backends
2727
patch()
28-
self.tracer = get_dummy_tracer()
2928
self.pin = Pin(service='celery-unittest', tracer=self.tracer)
3029
self.app = Celery('celery.test_app', broker=BROKER_URL, backend=BACKEND_URL)
3130
# override pins to use our Dummy Tracer
@@ -39,6 +38,8 @@ def tearDown(self):
3938
config.celery.update(self._config)
4039
self._config = None
4140

41+
super(CeleryBaseTestCase, self).tearDown()
42+
4243
def assert_items_equal(self, a, b):
4344
if PY2:
4445
return self.assertItemsEqual(a, b)

tests/contrib/celery/test_utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def fn_task():
8989
# delete the Span
9090
weak_dict = getattr(fn_task, '__dd_task_span')
9191
detach_span(fn_task, task_id)
92-
ok_(weak_dict.get(task_id) is None)
92+
ok_(weak_dict.get((task_id, False)) is None)
9393

9494
def test_span_delete_empty(self):
9595
# ensure the helper works even if the Task doesn't have
@@ -119,13 +119,14 @@ def fn_task():
119119
task_id = '7c6731af-9533-40c3-83a9-25b58f0d837f'
120120
attach_span(fn_task, task_id, self.tracer.trace('celery.run'))
121121
weak_dict = getattr(fn_task, '__dd_task_span')
122-
ok_(weak_dict.get(task_id))
122+
key = (task_id, False)
123+
ok_(weak_dict.get(key))
123124
# flush data and force the GC
124-
weak_dict.get(task_id).finish()
125+
weak_dict.get(key).finish()
125126
self.tracer.writer.pop()
126127
self.tracer.writer.pop_traces()
127128
gc.collect()
128-
ok_(weak_dict.get(task_id) is None)
129+
ok_(weak_dict.get(key) is None)
129130

130131
def test_task_id_from_protocol_v1(self):
131132
# ensures a `task_id` is properly returned when Protocol v1 is used.

0 commit comments

Comments
 (0)