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

AsyncioScopeManager based on contextvars and supporting Tornado 6 #118

Merged

Conversation

condorcet
Copy link
Contributor

Python 3.7 provides contextvars that can be used for implicitly propagation of context from parent to child coroutines / tasks.
Implementation of AsyncioScopeManager in the PR based on this feature so no more need to make manual activation of parent span in child coroutine / tasks like this:

with tracer.scope_manager.activate(parent_span, finish_on_close=False):
                ...
                with tracer.start_active_span('child') as scope:
                    ...

For python 3.6 we have backport of contextvars with async / await supporting https://github.com/fantix/aiocontextvars that based on the "official" backport https://github.com/MagicStack/contextvars. So in python 3.6 "new" AsyncioScopeManager also works.

Testbed contains tests for "new" AsyncioScopeManager and compatibility tests for original scope manager.

On the other side Tornado >= 6 became fully asyncio-based framework (that why stack_context was deprecated) so we no need to support TornadoScopeManager for these versions. I think the better way is to use AsyncioScopeManager in new Tornado applications instead of backporting stack_context or something like this in TornadoScopeManager specially for opentracing needs.
Of course for Tornado < 6 we have to use TornadoScopeManager and it still here.

@carlosalberto
Copy link
Contributor

Hey hey @condorcet awesome, looks pretty neat. I will get back for a full review in the next days. One thing to maybe do, is to rename the 'new' AsyncioScopeManager to ContextScopeManager or something, as it is actually more related to contextvars ;)

(also, this way it won't be weird to use it with Tornado apps ;) )

@condorcet
Copy link
Contributor Author

@carlosalberto I thought about backward compatibility between new and old scope manager, that's why I made changes right into AsyncioScopeManager. As I understand you would prefer another one context manager (something like ContextScopeManager) that will be separated from existing AsyncioScopeManager. Also it should contain their own testbed. Am I right?

@condorcet
Copy link
Contributor Author

@carlosalberto ping

@carlosalberto
Copy link
Contributor

Hey @condorcet

Sorry for the delayed answer. So keeping backwards compatibility is nice - so what about having the new manager be ContextScopeManager, and have AsyncioContextManager be a child of it with, without any items? This way it would be the actual same implementation, but we would keep the old AsyncioScopeManager name around ;)

class AsyncioScopeManager(ContextScopeManager):
   """ Documentation mentioning this now inherits from ContextScopeManager, etc
   """
   pass

We can keep the old TornadoScopeManager for tornado < 6, and tell users to use the new ContextScopeManager for >= 6.

Also, we can keep a single testbed for ContextScopeManager/AsyncioScopeManager ;)

Let me know if this sounds reasonable for you - else, feel free to comment :)

@Jamim
Copy link
Contributor

Jamim commented Jun 14, 2019

ping @condorcet

@condorcet
Copy link
Contributor Author

Hello @carlosalberto thank you for answer.
Let me explain my doubt.
I have the following picture in my head:

  • Tornado 5 (with async/await style, not "old" gen.coroutine style) and Tornado 6 should using AsyncioScopeManager, because of using asyncio loop and async / await syntax. This scope manager common by design for any async/await code, particular new Tornado apps.

  • Tornado 4 and Tornado 5 (with old gen.coroutine style) should using special TornadoScopeManager with narrow scope of usage.

If we make special ContextScopeManager for Tornado >= 6 I'm afraid it can confuse users -- another one scope manager without any difference AsyncioScopeManager, but should be used only in Tornado apps. Because under the hood Tornado >=6 works as any other asyncio-based framework it will be better to use exactly AsyncioScopeManager. ContextScopeManager in this case will be redundant.

What do you think?

@condorcet
Copy link
Contributor Author

ping @carlosalberto

@@ -14,6 +14,11 @@
# limitations under the License.

from __future__ import absolute_import
try:
# Contextvars backport with coroutine supporting (python 3.6).
import aiocontextvars # noqa
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not try this import from opentracing.scope_managers.asyncio?


install:
- make bootstrap
- pip install -q "tornado$TORNADO"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it's safer to do this before make bootstrap ;)

Automatic :class:`~opentracing.Span` propagation from
parent coroutines to their children is not provided, which needs to be
done manually:
The scope manager provides automatic :class:`~opentracing.Span` propagation
Copy link
Contributor

@carlosalberto carlosalberto Jul 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one reason that makes me feel we should keep this new implementation under ContextVarScopeManager or similar - otherwise, we would be breaking the assumption that manual propagation is required (we could at the same time deprecate AsyncioScopeManager in favor of this new one, of course).

At the same time, does aiocontextvars work for Python 3.4/3.5? If not, we cannot use them with asyncio (I bring this up as I've talked to people and the need to still keep Python 3.4 backwards compatibility).

(I we are going to take this route, we would need, yeah, a section for contextvars in the testbed :( )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @carlosalberto !

...otherwise, we would be breaking the assumption that manual propagation is required (we could at the same time deprecate AsyncioScopeManager in favor of this new one, of course).

That's what I meant about backward compatibility of new scope manager :) No need to activate parent span, but you can do it and it doesn't break behavior of existing user code. There are tests that prove it.

I bring this up as I've talked to people and the need to still keep Python 3.4 backwards compatibility

Wow! But this is really strong reason to separate new scope manager, because both libraries (contextvars and aiocontextvars) doesn't support python 3.4. So this brave new ContextVarScopeManager we have to use only with python >= 3.5

we could at the same time deprecate AsyncioScopeManager in favor of this new one, of course

Maybe we can release new scope manager in new major version of the library (3.0.0) with renamed existing AsyncioScopeManager -> OldAsyncioScopeManager? Yes, it will break backward compatibility for python 3.4 users, but it's ok in terms of semver.

@carlosalberto
Copy link
Contributor

carlosalberto commented Jul 11, 2019

Hey @condorcet

Sorry for the delayed answer - a mix of hectic days (which included talking to people on how to manage things with the nice incoming contextvar module), along with holidays got in the way.

I left a pair of comments. Let me know what you think (as I won't be taking long holidays anytime soon, I should be able to be super responsive now - I can also help with the testbed part too ;) )

@condorcet
Copy link
Contributor Author

@carlosalberto We definitely have to separate ContextVarScopeManager from AsyncioScopeManager: I didn't consider situation with callbacks / tasks that scheduling in event loop in some coroutine which already have parent context. And with contextvars this parent span will propagate to the scheduled callback / task. It's not expected behavior and may break tracing when someone use original AsyncioScopeManager, where auto-propagation doesn't work and new task will be created without parent.

Another feature we should remember when using ContextVarScopeManager is periodic callbacks / tasks. For example :

async def task():
            await asyncio.sleep(5)
            with self.tracer.start_active_span('periodic_task'):
                # next iteration of `task` takes parent context from previous iteration
                # and it will go on forever.
                await task()

Of course it's very synthetic and rude: you shouldn't await task() right into context or more preferred you should use ignore_active_span=True in context manager. But user should be aware about the feature when making tracing wrappers for existing code.

I have some draft and hope to commit it in next days.

@condorcet condorcet force-pushed the asyncio-context-with-context-vars branch from a3bc006 to 376c91d Compare July 26, 2019 09:42
@condorcet
Copy link
Contributor Author

Hello @carlosalberto !
I made ContextVarScopeManager that separated from AsyncioScopeManager. Theoretically new scope manager is more general and can be used instead of ThreadLocalScopeManager, because of contextvars, but we should prove it by tests. In this PR I've only added tests and testbed for new scope manager with asyncio.

Also I removed contextvars backport for following reasons:

  1. It doesn't work with python < 3.5.
  2. It doesn't support context propagation for methods like call_at (but we can achieve this feature by the PR Patch call_soon and friends. fantix/aiocontextvars#66) so behavior in python 3.5/3.6 and 3.7 will be different.
    I think this reasons can confuse users, so better to keep it simple and mention that ContextVarScopeManager can be used only in Python 3.7+.

Also I've added some new tests for case when parent context propagates to coroutines / callbacks that was scheduled in event loop by call_at and other methods. You can see tests here 376c91d but I don't think that it's the best place for them.

no_parent_scope is context manager that can "reset" current scope: 376c91d#diff-f45867ddcb1eafc26fc3a08eaa762ae3R110
It also has tests. I don't have strong opinion about useful of this helper :)

@Jamim
Copy link
Contributor

Jamim commented Aug 28, 2019

Hello @condorcet,

You probably might help with the similar work for OpenTelemetry which is the next major version of the OpenTracing and OpenCensus projects.
If so, could you please take a look at open-telemetry/opentelemetry-python#92?

Thank you!

@Jamim
Copy link
Contributor

Jamim commented Aug 31, 2019

Hello @carlosalberto,
Can you please give @condorcet some additional feedback?
Thanks!



@contextmanager
def no_parent_scope():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not simply provide default=None for _SCOPE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not enough. I've added docstring to make the idea clear. Sometimes we can have a scope, that we don't want to propagate.

_SCOPE = ContextVar('scope')


class ContextVarsScopeManager(ThreadLocalScopeManager):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to inherit from ThreadLocalScopeManager, I think (we are not calling into it, and contextvars itself works fine with thread-local data).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -28,6 +28,10 @@ No automatic `Span` propagation between parent and children tasks is provided, a

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

### contextvars

`ContextVarsScopeManager` uses [contextvars](https://docs.python.org/3/library/contextvars.html) module to both store **and** automatically propagate the context from parent coroutines / tasks / scheduled in event loop callbacks to their children.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC contextvars automatically propagates context only when used with asyncio - else, the user needs to propagate himself the context (through contextvars.copy_context()). Maybe we could add a note here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean explicit span propagation in multi-threaded code? Because you don't need to do this in single-threaded code, it works implicitly (like threading.local). Or I misunderstood your idea?

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

unittest.TextTestRunner(verbosity=3).run(main_suite)
result = unittest.TextTestRunner(verbosity=3).run(main_suite)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@@ -44,7 +44,7 @@ def submit_subtasks(self, parent_span):
logger.info('Running %s' % name)
with self.tracer.scope_manager.activate(parent_span, False):
with self.tracer.start_active_span(name):
asyncio.sleep(0.1)
await asyncio.sleep(0.1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

def assertNamesEqual(self, spans, names):
self.assertEqual(list(map(lambda x: x.operation_name, spans)), names)

def assertEmptySpan(self, span, name):
Copy link
Contributor

@carlosalberto carlosalberto Sep 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave the name assertion out of this test method ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's super convenient to assert span with name, because every span must has name :) I can make name optional in this helper.

@carlosalberto
Copy link
Contributor

Hey @condorcet

Sorry for the delay, had to look for some cycles to review (and play) with your code ;) it all looks good, thanks a lot!

I left a few comments. Once that is resolved (along your own TODO notes), we should be ready to merge ;)

@condorcet condorcet force-pushed the asyncio-context-with-context-vars branch from 376c91d to c4468e7 Compare September 9, 2019 20:07
@condorcet
Copy link
Contributor Author

Hello @carlosalberto thank you for your feedback!
I've made some fixes and rebase this branch on master.

I need your help with this bunch of contextvar-specific tests https://github.com/opentracing/opentracing-python/pull/118/files#diff-bd336347a31f644e377b29aa0b028962R64 Where should I move them? Or just leave them in this file?

@condorcet
Copy link
Contributor Author

Hi @carlosalberto ! Please could you take a look at fixes?
Thanks in advance!

@Jamim
Copy link
Contributor

Jamim commented Oct 10, 2019

ping @carlosalberto

@rhilfers
Copy link

excited for this one.. bump @carlosalberto . Thanks @condorcet for the implementation!

@condorcet
Copy link
Contributor Author

@carlosalberto @yurishkuro Hi! What about the PR? While we don't have stable release of Open Telemetry which includes contextvars out of the box, it would be great to have this feature in OT. For example this feature was added by https://github.com/DataDog/dd-trace-py (which compatible with OT) on September.
What should we do next to merge it to master?

@yurishkuro
Copy link
Member

I have no experience with asyncio or contextvars, so cannot provide a review. However, it appears that this change is purely additive, so appears to be safe to merge.

Testbed contains tests for "new" AsyncioScopeManager and compatibility tests for original scope manager.

Do you mean that the test bed runs all the existing scope manager implementations as well as the new one?

@condorcet
Copy link
Contributor Author

Testbed contains tests for "new" AsyncioScopeManager and compatibility tests for original scope manager.

Do you mean that the test bed runs all the existing scope manager implementations as well as the new one?

@yurishkuro Yes, you're right, finally we have the new one scope manager ContextVarsScopeManager and test bed for it.

@sheregeda
Copy link

ping @carlosalberto

@nicholasamorim
Copy link

This would be massive for those using AsyncIO in Python3.7+

Copy link
Member

@yurishkuro yurishkuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the testbed design kind of weird, I was expecting some common code in each of the use cases, and each test for a specific context manager just providing some customizations. But it's not in scope of this PR to fix.

setup.py Show resolved Hide resolved
@@ -5,7 +5,8 @@ This example shows a `Span` used with `RequestHandler`, which is used as a middl
Implementation details:
- For `threading`, no active `Span` is consumed as the tasks may be run concurrently on different threads, and an explicit `SpanContext` has to be saved to be used as parent.
- For `gevent` and `asyncio`, as no automatic `Span` propagation is done, an explicit `Span` has to be saved to be used as parent (observe an instrumentation library could help to do that implicitly - we stick to the simplest case, though).
- For `tornado`, as the `StackContext` automatically propapates the context (even is the tasks are called through different coroutines), we **do** leverage the active `Span`.
- For `tornado` (as the `StackContext`) and `contextvars` automatically propagates the context (even is the tasks are called through different coroutines), we **do** leverage the active `Span`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sentence seems broken, eg. what is "(as the StackContext)"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thank you! I'll fix the typo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

testbed/test_multiple_callbacks/README.md Outdated Show resolved Hide resolved
testbed/test_nested_callbacks/README.md Outdated Show resolved Hide resolved
testbed/test_subtask_span_propagation/README.md Outdated Show resolved Hide resolved
@yurishkuro yurishkuro changed the title Proof of concept: AsyncioScopeManager based on contextvars and supporting Tornado 6 AsyncioScopeManager based on contextvars and supporting Tornado 6 Dec 10, 2019
condorcet and others added 5 commits December 10, 2019 19:35
Co-Authored-By: Yuri Shkuro <yurishkuro@users.noreply.github.com>
Co-Authored-By: Yuri Shkuro <yurishkuro@users.noreply.github.com>
Co-Authored-By: Yuri Shkuro <yurishkuro@users.noreply.github.com>
Co-Authored-By: Yuri Shkuro <yurishkuro@users.noreply.github.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants