Skip to content
This repository was archived by the owner on May 23, 2023. It is now read-only.

Commit a3bc006

Browse files
committed
Asyncio context manager with contextvars. Add different versions of tornado to travis.yml.
1 parent 35a7bc9 commit a3bc006

File tree

11 files changed

+118
-69
lines changed

11 files changed

+118
-69
lines changed

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ python:
88
- "3.7"
99
- "3.8-dev"
1010

11+
env:
12+
- TORNADO=">=4,<5"
13+
- TORNADO=">=5,<6"
14+
- TORNADO=">=6"
15+
1116
matrix:
1217
allow_failures:
1318
- python: "3.8-dev"
19+
exclude:
20+
- python: "2.7"
21+
env: TORNADO=">=6"
1422

1523
install:
1624
- make bootstrap
25+
- pip install -q "tornado$TORNADO"
1726

1827
script:
1928
- make test testbed lint

opentracing/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
# limitations under the License.
1515

1616
from __future__ import absolute_import
17+
try:
18+
# Contextvars backport with coroutine supporting (python 3.6).
19+
import aiocontextvars # noqa
20+
except ImportError:
21+
pass
1722
from .span import Span # noqa
1823
from .span import SpanContext # noqa
1924
from .scope import Scope # noqa

opentracing/scope_managers/asyncio.py

Lines changed: 18 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,29 @@
2020

2121
from __future__ import absolute_import
2222

23-
import asyncio
23+
from contextvars import ContextVar
2424

2525
from opentracing import Scope
2626
from opentracing.scope_managers import ThreadLocalScopeManager
27-
from .constants import ACTIVE_ATTR
27+
28+
29+
_SCOPE = ContextVar('scope')
2830

2931

3032
class AsyncioScopeManager(ThreadLocalScopeManager):
3133
"""
3234
:class:`~opentracing.ScopeManager` implementation for **asyncio**
33-
that stores the :class:`~opentracing.Scope` in the current
34-
:class:`Task` (:meth:`Task.current_task()`), falling back to
35-
thread-local storage if none was being executed.
35+
that stores the :class:`~opentracing.Scope` using ContextVar.
3636
37-
Automatic :class:`~opentracing.Span` propagation from
38-
parent coroutines to their children is not provided, which needs to be
39-
done manually:
37+
The scope manager provides automatic :class:`~opentracing.Span` propagation
38+
from parent coroutines to their children.
4039
4140
.. code-block:: python
4241
4342
async def child_coroutine(span):
44-
# activate the parent Span, but do not finish it upon
45-
# deactivation. That will be done by the parent coroutine.
46-
with tracer.scope_manager.activate(span, finish_on_close=False):
47-
with tracer.start_active_span('child') as scope:
48-
...
43+
# No need manual activation of parent span in child coroutine.
44+
with tracer.start_active_span('child') as scope:
45+
...
4946
5047
async def parent_coroutine():
5148
with tracer.start_active_span('parent') as scope:
@@ -63,24 +60,13 @@ def activate(self, span, finish_on_close):
6360
:param finish_on_close: whether *span* should automatically be
6461
finished when :meth:`Scope.close()` is called.
6562
66-
If no :class:`Task` is being executed, thread-local
67-
storage will be used to store the :class:`~opentracing.Scope`.
68-
6963
:return: a :class:`~opentracing.Scope` instance to control the end
7064
of the active period for the :class:`~opentracing.Span`.
7165
It is a programming error to neglect to call :meth:`Scope.close()`
7266
on the returned instance.
7367
"""
7468

75-
task = self._get_task()
76-
if not task:
77-
return super(AsyncioScopeManager, self).activate(span,
78-
finish_on_close)
79-
80-
scope = _AsyncioScope(self, span, finish_on_close)
81-
self._set_task_scope(scope, task)
82-
83-
return scope
69+
return self._set_scope(span, finish_on_close)
8470

8571
@property
8672
def active(self):
@@ -93,46 +79,25 @@ def active(self):
9379
or ``None`` if not available.
9480
"""
9581

96-
task = self._get_task()
97-
if not task:
98-
return super(AsyncioScopeManager, self).active
82+
return self._get_scope()
9983

100-
return self._get_task_scope(task)
84+
def _set_scope(self, span, finish_on_close):
85+
return _AsyncioScope(self, span, finish_on_close)
10186

102-
def _get_task(self):
103-
try:
104-
# Prevent failure when run from a thread
105-
# without an event loop.
106-
loop = asyncio.get_event_loop()
107-
except RuntimeError:
108-
return None
109-
110-
return asyncio.Task.current_task(loop=loop)
111-
112-
def _set_task_scope(self, scope, task=None):
113-
if task is None:
114-
task = self._get_task()
115-
116-
setattr(task, ACTIVE_ATTR, scope)
117-
118-
def _get_task_scope(self, task=None):
119-
if task is None:
120-
task = self._get_task()
121-
122-
return getattr(task, ACTIVE_ATTR, None)
87+
def _get_scope(self):
88+
return _SCOPE.get(None)
12389

12490

12591
class _AsyncioScope(Scope):
12692
def __init__(self, manager, span, finish_on_close):
12793
super(_AsyncioScope, self).__init__(manager, span)
12894
self._finish_on_close = finish_on_close
129-
self._to_restore = manager.active
95+
self._token = _SCOPE.set(self)
13096

13197
def close(self):
13298
if self.manager.active is not self:
13399
return
134-
135-
self.manager._set_task_scope(self._to_restore)
100+
_SCOPE.reset(self._token)
136101

137102
if self._finish_on_close:
138103
self.span.finish()

setup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
include_package_data=True,
2727
zip_safe=False,
2828
platforms='any',
29+
install_requires=[
30+
'futures;python_version=="2.7"',
31+
'aiocontextvars;python_version>="3.5"',
32+
],
2933
extras_require={
3034
'tests': [
3135
'doubles',
@@ -40,8 +44,7 @@
4044

4145
'six>=1.10.0,<2.0',
4246
'gevent',
43-
'tornado<6',
47+
'tornado',
4448
],
45-
':python_version == "2.7"': ['futures'],
4649
},
4750
)

testbed/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ Alternatively, due to the organization of the suite, it's possible to run direct
1818

1919
## Tested frameworks
2020

21-
Currently the examples cover `threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). Each example uses their respective `ScopeManager` instance from `opentracing.scope_managers`, along with their related requirements and limitations.
21+
Currently the examples cover from ..utils import get_one_by_operation_name, stop_loop_when
22+
`threading`, `tornado`, `gevent` and `asyncio` (which requires Python 3). Each example uses their respective `ScopeManager` instance from `opentracing.scope_managers`, along with their related requirements and limitations.
2223

23-
### threading, asyncio and gevent
24+
### threading and gevent
2425

2526
No automatic `Span` propagation between parent and children tasks is provided, and thus the `Span` need to be manually passed down the chain.
2627

28+
### asyncio
29+
30+
`AsyncioScopeManager` supports automatically propagate the context from parent coroutines to their children. For compatibility reasons with previous version of `AsyncioScopeManager`, asyncio testbed contains test cases showing that manual activation of parent span in child span also works as expected.
31+
2732
### tornado
2833

2934
`TornadoScopeManager` uses a variation of `tornado.stack_context.StackContext` to both store **and** automatically propagate the context from parent coroutines to their children.

testbed/__main__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
from importlib import import_module
22
import logging
33
import os
4+
import sys
45
import six
56
import unittest
7+
from tornado import version_info as tornado_version
68

79

810
enabled_platforms = [
911
'threads',
10-
'tornado',
1112
'gevent',
1213
]
14+
if tornado_version < (6, 0, 0, 0):
15+
# Including testbed for Tornado coroutines and stack context.
16+
# We don't need run testbed in case Tornado>=6, because it became
17+
# asyncio-based framework and `stack_context` was deprecated.
18+
enabled_platforms.append('tornado')
1319
if six.PY3:
1420
enabled_platforms.append('asyncio')
1521

@@ -47,4 +53,6 @@ def get_test_directories():
4753
suite = loader.loadTestsFromModule(test_module)
4854
main_suite.addTests(suite)
4955

50-
unittest.TextTestRunner(verbosity=3).run(main_suite)
56+
result = unittest.TextTestRunner(verbosity=3).run(main_suite)
57+
if result.failures or result.errors:
58+
sys.exit(1)

testbed/test_late_span_finish/test_asyncio.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ def setUp(self):
1818

1919
def test_main(self):
2020
# Create a Span and use it as (explicit) parent of a pair of subtasks.
21-
parent_span = self.tracer.start_span('parent')
21+
parent_scope = self.tracer.start_active_span('parent')
22+
parent_span = parent_scope.span
2223
self.submit_subtasks(parent_span)
2324

2425
stop_loop_when(self.loop,
2526
lambda: len(self.tracer.finished_spans()) >= 2)
2627
self.loop.run_forever()
2728

2829
# Late-finish the parent Span now.
29-
parent_span.finish()
30+
parent_scope.close()
3031

3132
spans = self.tracer.finished_spans()
3233
self.assertEqual(len(spans), 3)
@@ -44,7 +45,19 @@ async def task(name):
4445
logger.info('Running %s' % name)
4546
with self.tracer.scope_manager.activate(parent_span, False):
4647
with self.tracer.start_active_span(name):
47-
asyncio.sleep(0.1)
48+
await asyncio.sleep(0.1)
49+
50+
self.loop.create_task(task('task1'))
51+
self.loop.create_task(task('task2'))
52+
53+
54+
class TestAutoContextPropagationAsyncio(TestAsyncio):
55+
56+
def submit_subtasks(self, parent_span):
57+
async def task(name):
58+
logger.info('Running %s' % name)
59+
with self.tracer.start_active_span(name):
60+
await asyncio.sleep(0.1)
4861

4962
self.loop.create_task(task('task1'))
5063
self.loop.create_task(task('task2'))

testbed/test_multiple_callbacks/test_asyncio.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ def submit_callbacks(self):
5757
tasks.append(t)
5858

5959
return tasks
60+
61+
62+
class TestAutoContextPropagationAsyncio(TestAsyncio):
63+
64+
async def task(self, interval, parent_span):
65+
logger.info('Starting task')
66+
with self.tracer.start_active_span('task'):
67+
await asyncio.sleep(interval)

testbed/test_nested_callbacks/test_asyncio.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,25 @@ async def task3():
5656
self.loop.create_task(task2())
5757

5858
self.loop.create_task(task1())
59+
60+
61+
class TestAutoContextPropagationAsyncio(TestAsyncio):
62+
63+
def submit(self):
64+
span = self.tracer.scope_manager.active.span
65+
66+
async def task1():
67+
span.set_tag('key1', '1')
68+
69+
async def task2():
70+
span.set_tag('key2', '2')
71+
72+
async def task3():
73+
span.set_tag('key3', '3')
74+
span.finish()
75+
76+
self.loop.create_task(task3())
77+
78+
self.loop.create_task(task2())
79+
80+
self.loop.create_task(task1())

testbed/test_subtask_span_propagation/test_asyncio.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import absolute_import, print_function
22

3-
import functools
4-
53
import asyncio
64

75
from opentracing.mocktracer import MockTracer
@@ -33,3 +31,10 @@ async def child_task(self, message, span):
3331
with self.tracer.scope_manager.activate(span, False):
3432
with self.tracer.start_active_span('child'):
3533
return '%s::response' % message
34+
35+
36+
class TestAutoContextPropagationAsyncio(TestAsyncio):
37+
38+
async def child_task(self, message, span):
39+
with self.tracer.start_active_span('child'):
40+
return '%s::response' % message

0 commit comments

Comments
 (0)