Opened 3 years ago

Closed 3 years ago

Last modified 3 years ago

#32815 closed Uncategorized (invalid)

Failed to reset ContextVars in sync/async middlewares

Reported by: Michael Manganiello Owned by: nobody
Component: Uncategorized Version: 3.2
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Michael Manganiello)

When using a middleware that can process both sync and async requests, and trying to set and reset a ContextVar (in different methods of its request lifecycle), Python fails with error:
ValueError: <Token var=<ContextVar name='current_context' at 0x7f9a8b9ad900> at 0x7f9a68575180> was created in a different Context

This is a simple middleware example to reproduce the mentioned issue:

import contextvars

current_context = contextvars.ContextVar('current_context')

@sync_and_async_middleware
class TemplateResponseMiddleware(BaseMiddleware):
    def process_view(self, request, view_func, view_args, view_kwargs):
        request.META['_CONTEXT_RESET_TOKEN'] = current_context.set(id(request))

    def process_template_response(self, request, response):
        current_context.reset(request.META['_CONTEXT_RESET_TOKEN'])
        return response

This use case is what the OpenTelemetry integration uses for spans to be traced in Django: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py

  • In process_request, a ContextVar is set, and the generated token is persisted in the request.META object.
  • In process_response, the ContextVar is reset, by using the persisted token.

This approach works correctly for synchronous requests. However, as part of adding ASGI support to the Django integration for OpenTelemetry (in https://github.com/open-telemetry/opentelemetry-python-contrib/pull/391), we found that the ContextVar triggers the mentioned error when we want to reset it to its previous value. OpenTelemetry inherits from MiddlewareMixin, but I'm attaching a diff for a simple test scenario that reproduces the issue, using the new Middleware format.

The main suspects here are the calls to sync_to_async, which adapt the middleware methods to the async flow. However, both those calls explicitly set thread_sensitive=True.

Traceback for the attached test scenario:

$ ./runtests.py -k MiddlewareSyncAsyncTests.test_async_process_template_response
# ...
ERROR: test_async_process_template_response (middleware_exceptions.tests.MiddlewareSyncAsyncTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mike/.virtualenvs/django/lib/python3.9/site-packages/asgiref/sync.py", line 222, in __call__
    return call_result.result()
  File "/usr/lib/python3.9/concurrent/futures/_base.py", line 438, in result
    return self.__get_result()
  File "/usr/lib/python3.9/concurrent/futures/_base.py", line 390, in __get_result
    raise self._exception
  File "/home/mike/.virtualenvs/django/lib/python3.9/site-packages/asgiref/sync.py", line 287, in main_wrap
    result = await self.awaitable(*args, **kwargs)
  File "/mnt/data/Proyectos/third_party/django/django/test/utils.py", line 423, in inner
    return await func(*args, **kwargs)
  File "/mnt/data/Proyectos/third_party/django/tests/middleware_exceptions/tests.py", line 319, in test_async_process_template_response
    response = await self.async_client.get(
  File "/mnt/data/Proyectos/third_party/django/django/test/client.py", line 911, in request
    self.check_exception(response)
  File "/mnt/data/Proyectos/third_party/django/django/test/client.py", line 580, in check_exception
    raise exc_value
  File "/home/mike/.virtualenvs/django/lib/python3.9/site-packages/asgiref/sync.py", line 458, in thread_handler
    raise exc_info[1]
  File "/mnt/data/Proyectos/third_party/django/django/core/handlers/exception.py", line 38, in inner
    response = await get_response(request)
  File "/mnt/data/Proyectos/third_party/django/django/core/handlers/base.py", line 249, in _get_response_async
    response = await middleware_method(request, response)
  File "/home/mike/.virtualenvs/django/lib/python3.9/site-packages/asgiref/sync.py", line 423, in __call__
    ret = await asyncio.wait_for(future, timeout=None)
  File "/usr/lib/python3.9/asyncio/tasks.py", line 442, in wait_for
    return await fut
  File "/home/mike/.virtualenvs/django/lib/python3.9/site-packages/asgiref/current_thread_executor.py", line 22, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/home/mike/.virtualenvs/django/lib/python3.9/site-packages/asgiref/sync.py", line 462, in thread_handler
    return func(*args, **kwargs)
  File "/mnt/data/Proyectos/third_party/django/tests/middleware_exceptions/middleware.py", line 135, in process_template_response
    current_context.reset(request.META['_CONTEXT_RESET_TOKEN'])
ValueError: <Token var=<ContextVar name='current_context' at 0x7f1dd4ec2720> at 0x7f1db129a880> was created in a different Context

Attachments (1)

django-issue-32815.diff (2.6 KB ) - added by Michael Manganiello 3 years ago.
Test scenario (diff over main commit f10c52afab)

Download all attachments as: .zip

Change History (6)

by Michael Manganiello, 3 years ago

Attachment: django-issue-32815.diff added

Test scenario (diff over main commit f10c52afab)

comment:1 by Michael Manganiello, 3 years ago

Description: modified (diff)

comment:2 by Michael Manganiello, 3 years ago

Description: modified (diff)

comment:3 by Michael Manganiello, 3 years ago

It seems this is not an issue only related to middlewares, but to how sync_to_async works in general? I am able to reproduce the same issue with this simple endpoint (when running Django using Gunicorn with Uvicorn workers):

import contextvars

from asgiref.sync import sync_to_async
from django.http import HttpResponse
from django.urls import path

current_context = contextvars.ContextVar('current_context')


async def healthcheck(request):
    token = await sync_to_async(current_context.set, thread_sensitive=True)(id(request))
    await sync_to_async(current_context.reset, thread_sensitive=True)(token)
    return HttpResponse('OK')

urlpatterns = [path('', healthcheck)]

comment:4 by Mariusz Felisiak, 3 years ago

Resolution: invalid
Status: newclosed

Thanks for this report, however it looks like a support question and Trac is not a support channel. Moreover you're discussing asgiref behavior not Django itself. I would recommend to open an issue in asgiref.

comment:5 by Michael Manganiello, 3 years ago

Thanks for the feedback. I've filed https://github.com/django/asgiref/issues/267 to find the root cause.

Note: See TracTickets for help on using tickets.
Back to Top