Global request object on ASGI

Hi,

I need to implement global request object function for purpose of my project:

  • for simple sync mode under WSGI - we are using CRequest-Middleware based on thread binding
  • but under async within ASGI mode it would not work and i need to find another solution for this task

I am not very familiar with internal logic of Django Async implementation and would be glad if someone could tell where to look for a solution

One option would be to store the request in a ContextVar that’s set/unset in the middleware similar to what CRequest-Middleware was doing.

Yeap, we did experiment with this idea yesterday, seems like it is working - but i am still not clearly understand ho)))

Under gunicorn/daphne/uwsgi local deployments showed stable result:

import contextvars

_request = contextvars.ContextVar('request', default=None)

import threading

import logging
import uuid


from django.conf import settings
from django.http import HttpResponse

from lamb.utils import dpath_value, LambRequest
from lamb.utils.transformers import transform_uuid
from lamb.middleware.async_mixin import AsyncMiddlewareMixin


logger = logging.getLogger(__name__)


class LambGRequestMiddleware(AsyncMiddlewareMixin):
    """
    Provides storage for the "current" request object, so that code anywhere
    in your project can access it, without it having to be passed to that code
    from the view.
    """

    def _call(self, request) -> HttpResponse:
        self.__class__.set_request(request)
        response = self.get_response(request)
        self.__class__.del_request()
        return response

    async def _acall(self, request) -> HttpResponse:
        self.__class__.set_request(request)
        response = await self.get_response(request)
        self.__class__.del_request()
        return response

    @classmethod
    def get_request(cls, default=None):
        return _request.get()

    @classmethod
    def set_request(cls, request):
        _request.set(request)

    @classmethod
    def del_request(cls):
        _request.set(None)


import asyncio
import logging

from django.http import HttpResponse

__all__ = ['AsyncMiddlewareMixin']


logger = logging.getLogger(__name__)


class AsyncMiddlewareMixin:
    sync_capable = True
    async_capable = True

    def __init__(self, get_response):
        if get_response is None:
            raise ValueError('get_response must be provided.')
        self.get_response = get_response
        self._async_check()
        super().__init__()

    def __repr__(self):
        return '<%s get_response=%s>' % (
            self.__class__.__qualname__,
            getattr(
                self.get_response,
                '__qualname__',
                self.get_response.__class__.__name__,
            ),
        )

    def _async_check(self):
        """
        If get_response is a coroutine function, turns us into async mode so
        a thread is not consumed during a whole request.
        """
        if asyncio.iscoroutinefunction(self.get_response):
            # Mark the class as async-capable, but do the actual switch
            # inside __call__ to avoid swapping out dunder methods
            self._is_coroutine = asyncio.coroutines._is_coroutine

    def __call__(self, request):
        # Exit out to async mode, if needed
        if asyncio.iscoroutinefunction(self.get_response):
            return self._acall(request)
        else:
            return self._call(request)

    def _call(self, request) -> HttpResponse:
        response = self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(response)
        return response

    async def _acall(self, request) -> HttpResponse:
        response = await self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(response)
        return response

Give this section of PEP567 a read. It discusses ContextVar and asyncio. If you have specific questions after that, please ask.

Yeah, thanks for link - had not expect that it would properly work even under sync mode, seems that under sync mode it wraps threads in same scheme, so at least uWSGI runner works well.