Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Context API #395

Merged
merged 62 commits into from
Feb 13, 2020
Merged

Adding Context API #395

merged 62 commits into from
Feb 13, 2020

Conversation

codeboten
Copy link
Contributor

@codeboten codeboten commented Feb 3, 2020

This change implements the Context API portion of OTEP #66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7.

Signed-off-by: Alex Boten aboten@lightstep.com

This change implements the Context API portion of OTEP open-telemetry#66. The CorrelationContext API and Propagation API changes will come in future PRs. We're leveraging entrypoints to support other implementations of the Context API if/when necessary. For backwards compatibility, this change uses aiocontextvars for Python versions older than 3.7.

Signed-off-by: Alex Boten <aboten@lightstep.com>
@codeboten codeboten requested a review from a team as a code owner February 3, 2020 23:42
Alex Boten added 2 commits February 3, 2020 16:03
@codeboten codeboten added the WIP Work in progress label Feb 4, 2020
Alex Boten added 2 commits February 4, 2020 08:02
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Alex Boten added 6 commits February 4, 2020 09:39
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
@codecov-io
Copy link

codecov-io commented Feb 4, 2020

Codecov Report

Merging #395 into master will increase coverage by 1.71%.
The diff coverage is 93.02%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #395      +/-   ##
=========================================
+ Coverage   86.58%   88.3%   +1.71%     
=========================================
  Files          39      42       +3     
  Lines        1998    2060      +62     
  Branches      231     236       +5     
=========================================
+ Hits         1730    1819      +89     
+ Misses        200     170      -30     
- Partials       68      71       +3
Impacted Files Coverage Δ
...telemetry-api/src/opentelemetry/context/context.py 100% <ø> (ø) ⬆️
...emetry-sdk/src/opentelemetry/sdk/trace/__init__.py 91.2% <ø> (+0.43%) ⬆️
...elemetry-api/src/opentelemetry/metrics/__init__.py 95.58% <100%> (+7.45%) ⬆️
...k/src/opentelemetry/sdk/metrics/export/__init__.py 100% <100%> (ø) ⬆️
...etry-sdk/src/opentelemetry/sdk/metrics/__init__.py 98.46% <100%> (-0.6%) ⬇️
.../src/opentelemetry/sdk/metrics/export/aggregate.py 100% <100%> (ø)
...y-api/src/opentelemetry/context/default_context.py 100% <100%> (+15%) ⬆️
...c/opentelemetry/sdk/context/contextvars_context.py 88.23% <100%> (+28.71%) ⬆️
...xt-zipkin/src/opentelemetry/ext/zipkin/__init__.py 84.41% <100%> (ø) ⬆️
...c/opentelemetry/sdk/context/threadlocal_context.py 100% <100%> (+31.25%) ⬆️
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f29111a...1f9bd76. Read the comment docs.

Alex Boten added 3 commits February 4, 2020 14:45
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
@codeboten codeboten added needs reviewers PRs with this label are ready for review and needs people to review to move forward. and removed WIP Work in progress labels Feb 5, 2020
_CONTEXT = None # type: typing.Optional[Context]


def _copy_context(context: typing.Optional[Context]) -> Context:
Copy link
Member

@Oberon00 Oberon00 Feb 5, 2020

Choose a reason for hiding this comment

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

I'd prefer writing these methods in the form

if context is None:
  context = get_current()
return some_action(context)

or even return some_action(context or get_current()).

EDIT: is (not) None is fine

return _CONTEXT # type: ignore


def set_current(context: Context) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

If this is supposed to represent the current context, then we cant just use a global variable. We need a context-local variable (or at least a thread local variable). But this is already what the context inside does, so it seems we have mixed up "current Context" and "configured context manager".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe my interpretation of what set_current is intended to achieve is wrong. My understanding is that set_current is used here to update the global context which can then be referenced other places in the application with updated values rather than updating the context within a context manager. This I think can be achieved with the global here, though I might be missing something.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Oberon00 , what do you mean with "configured context manager"?

Copy link
Member

@Oberon00 Oberon00 Feb 6, 2020

Choose a reason for hiding this comment

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

I think we have to distinguish four things:

  1. A single context object, which is basically just a String -> Object dictionary. There is only at most one value for each key in a particular context. I think we are currently missing that concept.
  2. The current value of a particular context variable/key in some context (what get_value returns).
  3. The context manager, which is responsible for determining what the "current" context is, creating a new context object based on the parent context if a new logical execution context is entered / get_current is called in a logical execution context where no context object exists yet, etc.. This is what we currently call (IMHO confusingly) "Context".
  4. The logical execution context, an abstract concept, not a class or object. E.g. a thread of execution, the context of a async task, a greenlet, etc. The context manager defines what an execution context is and ensures that there is a 1:1 mapping from logical execution contexts to context objects.

EDIT: set_context should then set the context object in the current logical execution context.

EDIT2: As a concrete example, consider setting a value in a context object and then passing it (e.g. via a queue) to another, already running, thread. In that case, the other thread must be able to read that value back. This is not currently the case, as our "Context" is in fact a context manager and will return the return value of get_value(key, context) is potentially different when called from different logical execution contexts, even for identical context arguments.

EDIT3: I asked the spec folks: open-telemetry/opentelemetry-specification#424 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some clarifications:

  1. A single context object, which is basically just a String -> Object dictionary. There is only at most one value for each key in a particular context. I think we are currently missing that concept.

This is currently represented by the Context interface which is implemented by either ContextVarsContext or ThreadLocalContext depending on the version of Python

  1. The context manager, which is responsible for determining what the "current" context is, creating a new context object based on the parent context if a new logical execution context is entered / get_current is called in a logical execution context where no context object exists yet, etc.. This is what we currently call (IMHO confusingly) "Context".

This is implemented via the get_current & set_current methods

Copy link
Contributor

Choose a reason for hiding this comment

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

I was about to comment the same. Regarding point 1, here is the class that represents the concept of a single context object. It is implemented here, here and here.

Regarding your first EDIT, there is nothing named set_context. Did you mean set_current?

Copy link
Member

@Oberon00 Oberon00 Feb 7, 2020

Choose a reason for hiding this comment

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

@codeboten @ocelotl: No, ContextVarsContext and ThradLocalContext are not a contexts in the OTEP 66 sense. E.g. ThreadLocalContext is a mapping (thread, key) -> value instead of just key -> value. That's what I wanted to clarify in my EDIT2.

For example, in the OTEP, Propagator.extract returns a context. Which return type would you use given the types in this PR, so that you can give the context to another thread or execution context and it still keeps its values?

Copy link
Member

Choose a reason for hiding this comment

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

I think @Oberon00 has a good point here, and this will not work as expected.

Using his context queue example:

from queue import Queue
import asyncio

from opentelemetry import context
from opentelemetry.sdk.context.contextvars_context import ContextVarsContext

context.set_current(ContextVarsContext())

q = Queue()

async def main():
    q.put(context.get_current().copy())
    q.put(context.get_current().copy())
    asyncio.gather(work(1), work(2))

async def work(val):
    context.set_current(q.get_nowait())
    context.get_current().set_value('k', val)
    # At this point context.get_current() should be the one we popped from the
    # queue, and current_context.get_value('k') should be `val`

await main()

This works as expected, but only incidentally. If the call to set_current happens in one task in between the set_current and get_current calls in another, we get some weird behavior. Running effectively the same code, but manipulating the call order with an Event shows this:

from queue import Queue
from threading import Event
import asyncio

from opentelemetry import context
from opentelemetry.sdk.context.contextvars_context import ContextVarsContext

context.set_current(ContextVarsContext())

# Helper to print Contexts
def tos(context):
    return "<Context at {}: {}>".format(
        hex(id(context)),
        ','.join(["{}={}".format(k, v.get(None))
                  for k, v in context._contextvars.items()])
    )

q = Queue()
run1 = Event()
run2 = Event()

async def main():
    q.put(context.get_current().copy())
    q.put(context.get_current().copy())
    asyncio.gather(a1(), a2())

async def a1():
    # (Step 1)
    context.set_current(q.get_nowait())
    context.get_current().set_value('k', 1)

    # Expected context is current, k=1
    print("[task 1] {}".format(tos(context.get_current())))

    # (GOTO 2)
    run2.set(); await asyncio.sleep(0); run1.wait();

    # (Step 3)
    # Current context has changed AND k=None because the call to `set` k=2
    # happened in the other task!
    print("[task 1] {}".format(tos(context.get_current())))


async def a2():
    run2.wait()

    # (Step 2)
    context.set_current(q.get_nowait())
    context.get_current().set_value('k', 2)

    # Expected context is current, k=2
    print("[task 2] {}".format(tos(context.get_current())))

    # (GOTO 3)
    run1.set(); await asyncio.sleep(0);

await main()

This prints:

[task 1] <Context at 0x110349dd0: k=1>
[task 2] <Context at 0x110349250: k=2>
[task 1] <Context at 0x110349250: k=None>

Task 1 gets the context from the set_current call in task 2, but doesn't get its context variable values because set calls are scoped to the task! You could show roughly the same thing with threads and threadlocals.

(Sorry for the long example here)

Copy link
Member

Choose a reason for hiding this comment

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

@ocelotl Yes, sorry I meant set_current.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank for the clarifications. I've updated the Context class to be an immutable object. The portion of the Context class that was used to wrap threadlocal or contextvars implementation details is now RuntimeContext.

I still need to update the PR to not use a global _CONTEXT variable, but I think this change is at least a step in the right direction.

opentelemetry-api/src/opentelemetry/context/__init__.py Outdated Show resolved Hide resolved
opentelemetry-api/src/opentelemetry/context/__init__.py Outdated Show resolved Hide resolved
@Oberon00
Copy link
Member

Oberon00 commented Feb 5, 2020

Requesting changes mainly because of #395 (comment)

tox.ini Outdated Show resolved Hide resolved
Alex Boten added 3 commits February 5, 2020 09:20
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
tox.ini Show resolved Hide resolved
Alex Boten added 2 commits February 5, 2020 09:52
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Alex Boten added 2 commits February 11, 2020 13:43
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Copy link
Member

@mauriciovasquezbernal mauriciovasquezbernal left a comment

Choose a reason for hiding this comment

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

Some very small nits I found around. I think we are close to get this finished.

I just want to notice that the current PR is changing the way contextvars and thread locals are used. Before this PR, a contextvar / threadlocal object was used to save each element in the context, for instance the current span were saved in its own contextvar / threadlocal. This PR is using a single contextvar / threadlocal to save the whole context object.

I'm a little bit afraid of the overhead of this solution, for instance setting the active span now requires to create a new context (copying all the elements of this) and then set it as current. However this is not a blocking comment, probably we could improve this later on if we really find it is a problem.

opentelemetry-api/src/opentelemetry/context/__init__.py Outdated Show resolved Hide resolved
opentelemetry-api/src/opentelemetry/context/context.py Outdated Show resolved Hide resolved
context.set_current(context.set_value("say", "bar"))


class TestAsyncio(unittest.TestCase):

Choose a reason for hiding this comment

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

I'm not totally sure this is the right place for this test. It is not strictly related with context, probably should be moved to the tracer tests.

codeboten and others added 4 commits February 12, 2020 08:57
Co-Authored-By: Mauricio Vásquez <mauricio@kinvolk.io>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Signed-off-by: Alex Boten <aboten@lightstep.com>
Copy link
Member

@mauriciovasquezbernal mauriciovasquezbernal left a comment

Choose a reason for hiding this comment

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

I think it's good to go, otherwise we'll remain blocked forever in this one.

Signed-off-by: Alex Boten <aboten@lightstep.com>
Copy link
Member

@c24t c24t left a comment

Choose a reason for hiding this comment

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

Tests look good, switching to an immutable Context looks like it solves the problem of values leaking between tasks, and loading the context class via entry_points looks like a great idea. No more substantial comments from me, this LGTM to merge.

I don't know about the utility of having separate API and SDK classes. Is there a use case where we'd rather have a DefaultRuntimeContext than a ContextVarsRuntimeContext? I don't think the arguments for no-op Tracer and Meter apply the same way to the context.

One thing I'd still like to see here in a comment or PR description for documentation, because it's not clear to me even after several reviews:

  • what does this do that the old AsyncRuntimeContext couldn't (and vice versa)?
  • what does this do that bare ContextVars couldn't (and vice versa)?

@@ -22,11 +22,12 @@
from types import TracebackType
from typing import Iterator, Optional, Sequence, Tuple, Type

from opentelemetry import context as context_api
Copy link
Member

Choose a reason for hiding this comment

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

If the alias is to avoid shadowing with Span.context below, maybe now is a good time to rename it Span.span_context.

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'd like to propose making this change along with the Propagation API change, which will update this code anyways. #415

@@ -578,7 +577,7 @@ def __init__(
):
# TODO: How should multiple TracerSources behave? Should they get their own contexts?
# This could be done by adding `str(id(self))` to the slot name.
self._current_span_slot = Context.register_slot("current_span")
self.key = get_span_key(tracer_source_id=str(id(self)))
Copy link
Member

Choose a reason for hiding this comment

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

Do we ever expect to have more than one TracerSource? I expected TracerSource to be a singleton, and all tracers to share the same current span .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My takeaway from the SIG meeting this morning was that we expect a single TracerSource, so this change and the comment above could be removed if that's the case.

opentelemetry-api/src/opentelemetry/context/__init__.py Outdated Show resolved Hide resolved
Signed-off-by: Alex Boten <aboten@lightstep.com>
@ocelotl ocelotl self-requested a review February 13, 2020 21:32
@codeboten
Copy link
Contributor Author

I don't know about the utility of having separate API and SDK classes. Is there a use case where we'd rather have a DefaultRuntimeContext than a ContextVarsRuntimeContext? I don't think the arguments for no-op Tracer and Meter apply the same way to the context.

I'm in agreement here, having the ContextVarsRuntimeContext in the SDK may not be useful. Having the RuntimeContext interface be available and configurable through entry_points gives us enough flexibility to support alternative implementations in the future. That being said, I would vote in favour of moving ContextVarsRuntimeContext and ThreadLocalRuntimeContext into the api, to reduce the amount of work users need to do in order to get a useful implementation. This could be revisited once we have a mechanism for simplifying configuration.

One thing I'd still like to see here in a comment or PR description for documentation, because it's not clear to me even after several reviews:

* what does this do that the old `AsyncRuntimeContext` couldn't (and vice versa)?

This implementation gives us the flexibility of supporting additional RuntimeContext without additional changes in the project. Third parties could provide a new implementation for RuntimeContext and make it available through entry_points and environment variable configuration without modifying the api or sdk. This would not have been the case previously.

* what does this do that bare `ContextVars` couldn't (and vice versa)?

If we did not plan on supporting Python 3.4, the Context API could potentially have leveraged ContextVars directly for all cases. Though I suppose this would have prevented us from supporting certain concurrency implementations (ie. older versions of tornado.concurrent).

@codeboten codeboten mentioned this pull request Feb 13, 2020
34 tasks
@c24t c24t removed the needs reviewers PRs with this label are ready for review and needs people to review to move forward. label Feb 13, 2020
@c24t c24t merged commit 72862c9 into open-telemetry:master Feb 13, 2020
toumorokoshi pushed a commit to toumorokoshi/opentelemetry-python that referenced this pull request Feb 17, 2020
This change implements the Context API portion of OTEP open-telemetry#66. The
CorrelationContext API and Propagation API changes will come in future PRs.
We're leveraging entrypoints to support other implementations of the Context
API if/when necessary. For backwards compatibility, this change uses
aiocontextvars for Python versions older than 3.7.

Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com>
Co-authored-by: Mauricio Vásquez <mauricio@kinvolk.io>
@toumorokoshi toumorokoshi mentioned this pull request Mar 9, 2020
srikanthccv pushed a commit to srikanthccv/opentelemetry-python that referenced this pull request Nov 1, 2020
* refactor: move Resource to @opentelemetry/sdk-base

Moves the Resource interface to the SDK level by creating a new packaged
called sdk-base. Also helps resolve the issue of shared objects between
the metrics and trace SDK.

* ftb

* bump version to 0.1.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants