Skip to content

Scopes aren't async safe #147

Description

@airhorns

We sadly started getting popped wrong scope assertion errors in our Django + channels application after upgrading to sentry-sdk from raven. Here's our stack:

django==2.1.2
channels==2.1.5
daphne==2.2.2
sentry-sdk==0.4.1

on Python 3.7.0.

Channels is great for us because we're building a websocket heavy application that does lots of work outside an HTTP request response cycle. We would like to use Sentry's scopes to decorate any exceptions that happen during that websocket processing with context from our application as Sentry already does for HTTP requests. We use the (confusingly named) channels self.scope to store stuff about which user and which session the websocket is for, and then we want to push that onto a Sentry scope every time we go to process a websocket frame that has arrived. We implemented a handy dandy asynccontextmanager that does exactly this.

This doesn't quite work right now however. I believe that because there are multiple coroutines executing at the same time, the ScopeManager contextmanager __exit__s get called at weird and unexpected times, especially if the coroutine execution is interleaved. I am not exactly sure, but I think that it would need to use a ContextVar the same way the HubManager does to be async friendly.

Here's a reproduction that triggers the same issue without all the django business:

import asyncio
import time
from sentry_sdk import push_scope
from contextlib import contextmanager, asynccontextmanager


@asynccontextmanager
async def set_context(value):
    with push_scope() as sentry_scope:
        sentry_scope.set_tag("value", value)
        yield


async def say_after(delay, what):
    async with set_context(what):
        await asyncio.sleep(delay)
        print(what)
        await asyncio.sleep(delay)
        print(f"{what} is done now")

async def main():
    print('started at', time.strftime('%X'))

    a = asyncio.create_task(say_after(0.25, 'hello'))
    b = asyncio.create_task(say_after(0.3, 'world'))
    c = asyncio.create_task(say_after(0.4, 'good day'))

    await a
    await b
    await c

    print('finished at', time.strftime('%X'))

asyncio.run(main())

Because the context exits happen in a different order than the enters, we get a AssertionError: popped wrong scope.

Happy to provide more information if this isn't clear! Multithreading strikes again!

Metadata

Metadata

Assignees

No one assigned
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions