Skip to content

Are we expected to be able to have working ContextVars when using sync_to_async #534

@rtpg

Description

@rtpg

I am trying to debug an issue I'm seeing in OTel's Django Instrumentation Library, and wanted to get clarity on whether the following sort of usage of ContextVar is expected to work:

(The following fails in 3.7.2 and 3.10.0 for us)

import asyncio
from asgiref.sync import sync_to_async
from contextvars import ContextVar

currentuser = ContextVar("currentuser")

def process_request(request):
    print(f"Processing request...")
    request["reset_token"] = currentuser.set(request["username"])

def get_response(request):
    return f"Current user is {currentuser.get()}"

def process_response(request, response):
    print(f"Response was\n{response}")
    if "reset_token" in request:
        currentuser.reset(request["reset_token"])

# crashes when set to True
USE_SYNC_TO_ASYNC = True

async def main_loop():
    # set a sentinel value to confirm no leakage
    currentuser.set("<Unset>")
    request = {"username": "alice"}
    print(f"Current user is {currentuser.get()}")
    process_request(request)
    response = get_response(request)
    if USE_SYNC_TO_ASYNC:
        await sync_to_async(process_response)(request, response)
    else:
        process_response(request, response)
    print(f"Current user is {currentuser.get()}")


if __name__ == "__main__":
    result = asyncio.run(
        main_loop()
    )
    print("Done")

In the above I'm using a ContextVar to hold the currentuser (In OTel's case it's to hold tracing information). if process_response goes through sync_to_async then the token reset request fails because we're not in the same context.

My naive assumption is that Django middleware process_request and process_response would run in the same context, but it seems like there's some thread juggling going on Django-side based on logging of threading.get_native_id() that I patched into my problematic middleware, and now I'm unsure if any usage of ContextVar within the Django stack is accepted.

(asgiref.local.Local seems to work fine, though)

Anyways I'm unsure if I shouldn't expect ContextVar to work within any sync_to_async-comingled code bases. Or if this is expected to work, and isn't.


For more details on the OTel issue properly said, I have, in _DjangoMiddleware.process_request:

        activation = use_span(span, end_on_exit=True)
        activation.__enter__()  # pylint: disable=E1101

^ span information is held in a ContextVar. Then, over in _DjangoMiddleware.process_response:

activate.__exit__(None, None, None)

Some logging of threading.get_native_id() seems to indicate that sometimes I am ending up in different threads, and hitting an issue. It's odd to me that that this is a "sometimes" thing, but in the recent past we hit another issue around asgiref.Locals leaking due (we believe) to Django's core handler doing:

                response = await sync_to_async(
                    response_for_exception, thread_sensitive=False
                )(request, exc)

So I'm thinking that some code paths are ending up with a request's request-side middleware running in one thread, and then the response-side middleware running in another thread. Not really asking to debug that underlying issue though, if ContextVar is suspect then we probably want to swap out OTel's context management to use asgiref.Local rather than contextvars.ContextVar

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions