Skip to content

Commit c8b1f40

Browse files
authored
Merge pull request #809 from DataDog/0.20.3-dev
Release 0.20.3
2 parents 65864ce + 3be17a4 commit c8b1f40

File tree

10 files changed

+376
-116
lines changed

10 files changed

+376
-116
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.2'
7+
__version__ = '0.20.3'
88

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

ddtrace/context.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,16 @@ class ThreadLocalContext(object):
239239
def __init__(self):
240240
self._locals = threading.local()
241241

242+
def _has_active_context(self):
243+
"""
244+
Determine whether we have a currently active context for this thread
245+
246+
:returns: Whether an active context exists
247+
:rtype: bool
248+
"""
249+
ctx = getattr(self._locals, 'context', None)
250+
return ctx is not None
251+
242252
def set(self, ctx):
243253
setattr(self._locals, 'context', ctx)
244254

ddtrace/contrib/asyncio/provider.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,8 @@ class AsyncioContextProvider(DefaultContextProvider):
2121
def activate(self, context, loop=None):
2222
"""Sets the scoped ``Context`` for the current running ``Task``.
2323
"""
24-
try:
25-
loop = loop or asyncio.get_event_loop()
26-
except RuntimeError:
27-
# detects if a loop is available in the current thread;
28-
# This happens when a new thread is created from the one that is running
29-
# the async loop
24+
loop = self._get_loop(loop)
25+
if not loop:
3026
self._local.set(context)
3127
return context
3228

@@ -35,19 +31,40 @@ def activate(self, context, loop=None):
3531
setattr(task, CONTEXT_ATTR, context)
3632
return context
3733

34+
def _get_loop(self, loop=None):
35+
"""Helper to try and resolve the current loop"""
36+
try:
37+
return loop or asyncio.get_event_loop()
38+
except RuntimeError:
39+
# Detects if a loop is available in the current thread;
40+
# DEV: This happens when a new thread is created from the out that is running the async loop
41+
# DEV: It's possible that a different Executor is handling a different Thread that
42+
# works with blocking code. In that case, we fallback to a thread-local Context.
43+
pass
44+
return None
45+
46+
def _has_active_context(self, loop=None):
47+
"""Helper to determine if we have a currently active context"""
48+
loop = self._get_loop(loop=loop)
49+
if loop is None:
50+
return self._local._has_active_context()
51+
52+
# the current unit of work (if tasks are used)
53+
task = asyncio.Task.current_task(loop=loop)
54+
if task is None:
55+
return False
56+
57+
ctx = getattr(task, CONTEXT_ATTR, None)
58+
return ctx is not None
59+
3860
def active(self, loop=None):
3961
"""
4062
Returns the scoped Context for this execution flow. The ``Context`` uses
4163
the current task as a carrier so if a single task is used for the entire application,
4264
the context must be handled separately.
4365
"""
44-
try:
45-
loop = loop or asyncio.get_event_loop()
46-
except RuntimeError:
47-
# handles RuntimeError: There is no current event loop in thread 'MainThread'
48-
# it happens when it's not possible to get the current event loop.
49-
# It's possible that a different Executor is handling a different Thread that
50-
# works with blocking code. In that case, we fallback to a thread-local Context.
66+
loop = self._get_loop(loop=loop)
67+
if not loop:
5168
return self._local.get()
5269

5370
# the current unit of work (if tasks are used)

ddtrace/contrib/futures/threading.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,24 @@ def _wrap_submit(func, instance, args, kwargs):
77
thread. This wrapper ensures that a new `Context` is created and
88
properly propagated using an intermediate function.
99
"""
10-
# propagate the same Context in the new thread
11-
current_ctx = ddtrace.tracer.context_provider.active()
10+
# If there isn't a currently active context, then do not create one
11+
# DEV: Calling `.active()` when there isn't an active context will create a new context
12+
# DEV: We need to do this in case they are either:
13+
# - Starting nested futures
14+
# - Starting futures from outside of an existing context
15+
#
16+
# In either of these cases we essentially will propagate the wrong context between futures
17+
#
18+
# The resolution is to not create/propagate a new context if one does not exist, but let the
19+
# future's thread create the context instead.
20+
current_ctx = None
21+
if ddtrace.tracer.context_provider._has_active_context():
22+
current_ctx = ddtrace.tracer.context_provider.active()
23+
24+
# If we have a context then make sure we clone it
25+
# DEV: We don't know if the future will finish executing before the parent span finishes
26+
# so we clone to ensure we properly collect/report the future's spans
27+
current_ctx = current_ctx.clone()
1228

1329
# extract the target function that must be executed in
1430
# a new thread and the `target` arguments
@@ -25,5 +41,6 @@ def _wrap_execution(ctx, fn, args, kwargs):
2541
provider sets the Active context in a thread local storage
2642
variable because it's outside the asynchronous loop.
2743
"""
28-
ddtrace.tracer.context_provider.activate(ctx)
44+
if ctx is not None:
45+
ddtrace.tracer.context_provider.activate(ctx)
2946
return fn(*args, **kwargs)

ddtrace/contrib/gevent/provider.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ class GeventContextProvider(BaseContextProvider):
1515
in the ``gevent`` library. Framework instrumentation that uses the
1616
gevent WSGI server (or gevent in general), can use this provider.
1717
"""
18+
def _get_current_context(self):
19+
"""Helper to get the current context from the current greenlet"""
20+
current_g = gevent.getcurrent()
21+
if current_g is not None:
22+
return getattr(current_g, CONTEXT_ATTR, None)
23+
return None
24+
25+
def _has_active_context(self):
26+
"""Helper to determine if we have a currently active context"""
27+
return self._get_current_context() is not None
28+
1829
def activate(self, context):
1930
"""Sets the scoped ``Context`` for the current running ``Greenlet``.
2031
"""
@@ -29,15 +40,15 @@ def active(self):
2940
uses the ``Greenlet`` class as a carrier, and everytime a greenlet
3041
is created it receives the "parent" context.
3142
"""
32-
current_g = gevent.getcurrent()
33-
ctx = getattr(current_g, CONTEXT_ATTR, None)
43+
ctx = self._get_current_context()
3444
if ctx is not None:
3545
# return the active Context for this greenlet (if any)
3646
return ctx
3747

3848
# the Greenlet doesn't have a Context so it's created and attached
3949
# even to the main greenlet. This is required in Distributed Tracing
4050
# when a new arbitrary Context is provided.
51+
current_g = gevent.getcurrent()
4152
if current_g:
4253
ctx = Context()
4354
setattr(current_g, CONTEXT_ATTR, ctx)

ddtrace/contrib/tornado/stack_context.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,41 @@ def __exit__(self, type, value, traceback):
5757
def deactivate(self):
5858
self._active = False
5959

60+
def _has_io_loop(self):
61+
"""Helper to determine if we are currently in an IO loop"""
62+
return getattr(IOLoop._current, 'instance', None) is not None
63+
64+
def _has_active_context(self):
65+
"""Helper to determine if we have an active context or not"""
66+
if not self._has_io_loop():
67+
return self._local._has_active_context()
68+
else:
69+
# we're inside a Tornado loop so the TracerStackContext is used
70+
return self._get_state_active_context() is not None
71+
72+
def _get_state_active_context(self):
73+
"""Helper to get the currently active context from the TracerStackContext"""
74+
# we're inside a Tornado loop so the TracerStackContext is used
75+
for stack in reversed(_state.contexts[0]):
76+
if isinstance(stack, self.__class__) and stack._active:
77+
return stack._context
78+
return None
79+
6080
def active(self):
6181
"""
6282
Return the ``Context`` from the current execution flow. This method can be
6383
used inside a Tornado coroutine to retrieve and use the current tracing context.
6484
If used in a separated Thread, the `_state` thread-local storage is used to
6585
propagate the current Active context from the `MainThread`.
6686
"""
67-
io_loop = getattr(IOLoop._current, 'instance', None)
68-
if io_loop is None:
87+
if not self._has_io_loop():
6988
# if a Tornado loop is not available, it means that this method
7089
# has been called from a synchronous code, so we can rely in a
7190
# thread-local storage
7291
return self._local.get()
7392
else:
7493
# we're inside a Tornado loop so the TracerStackContext is used
75-
for stack in reversed(_state.contexts[0]):
76-
if isinstance(stack, self.__class__) and stack._active:
77-
return stack._context
94+
return self._get_state_active_context()
7895

7996
def activate(self, ctx):
8097
"""
@@ -83,8 +100,7 @@ def activate(self, ctx):
83100
If used in a separated Thread, the `_state` thread-local storage is used to
84101
propagate the current Active context from the `MainThread`.
85102
"""
86-
io_loop = getattr(IOLoop._current, 'instance', None)
87-
if io_loop is None:
103+
if not self._has_io_loop():
88104
# because we're outside of an asynchronous execution, we store
89105
# the current context in a thread-local storage
90106
self._local.set(ctx)

ddtrace/provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class BaseContextProvider(object):
1010
* the ``active`` method, that returns the current active ``Context``
1111
* the ``activate`` method, that sets the current active ``Context``
1212
"""
13+
def _has_active_context(self):
14+
raise NotImplementedError
15+
1316
def activate(self, context):
1417
raise NotImplementedError
1518

@@ -32,6 +35,15 @@ class DefaultContextProvider(BaseContextProvider):
3235
def __init__(self):
3336
self._local = ThreadLocalContext()
3437

38+
def _has_active_context(self):
39+
"""
40+
Check whether we have a currently active context.
41+
42+
:returns: Whether we have an active context
43+
:rtype: bool
44+
"""
45+
return self._local._has_active_context()
46+
3547
def activate(self, context):
3648
"""Makes the given ``context`` active, so that the provider calls
3749
the thread-local storage implementation.

tests/base/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import contextlib
22
import unittest
33

4-
from ddtrace import config
4+
import ddtrace
55

66
from ..utils.tracer import DummyTracer
77
from ..utils.span import TestSpanContainer, TestSpan, NO_CHILDREN
@@ -30,7 +30,7 @@ def override_config(self, integration, values):
3030
>>> with self.override_config('flask', dict(service_name='test-service')):
3131
# Your test
3232
"""
33-
options = getattr(config, integration)
33+
options = getattr(ddtrace.config, integration)
3434

3535
original = dict(
3636
(key, options.get(key))
@@ -81,3 +81,13 @@ def assert_structure(self, root, children=NO_CHILDREN):
8181
"""Helper to call TestSpanNode.assert_structure on the current root span"""
8282
root_span = self.get_root_span()
8383
root_span.assert_structure(root, children)
84+
85+
@contextlib.contextmanager
86+
def override_global_tracer(self, tracer=None):
87+
original = ddtrace.tracer
88+
tracer = tracer or self.tracer
89+
setattr(ddtrace, 'tracer', tracer)
90+
try:
91+
yield
92+
finally:
93+
setattr(ddtrace, 'tracer', original)

0 commit comments

Comments
 (0)