Skip to content
Permalink
Browse files

Fixed #31224 -- Added support for asynchronous views and middleware.

This implements support for asynchronous views, asynchronous tests,
asynchronous middleware, and an asynchronous test client.
  • Loading branch information
andrewgodwin authored and felixxm committed Feb 12, 2020
1 parent 3f7e4b1 commit fc0fa72ff4cdbf5861a366e31cb8bbacd44da22d
@@ -15,6 +15,7 @@ class SessionMiddleware(MiddlewareMixin):
def __init__(self, get_response=None):
self._get_response_none_deprecation(get_response)
self.get_response = get_response
self._async_check()
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore

@@ -1,4 +1,3 @@
import asyncio
import logging
import sys
import tempfile
@@ -132,7 +131,7 @@ class ASGIHandler(base.BaseHandler):

def __init__(self):
super().__init__()
self.load_middleware()
self.load_middleware(is_async=True)

async def __call__(self, scope, receive, send):
"""
@@ -158,12 +157,8 @@ def __init__(self):
if request is None:
await self.send_response(error_response, send)
return
# Get the response, using a threadpool via sync_to_async, if needed.
if asyncio.iscoroutinefunction(self.get_response):
response = await self.get_response(request)
else:
# If get_response is synchronous, run it non-blocking.
response = await sync_to_async(self.get_response)(request)
# Get the response, using the async mode of BaseHandler.
response = await self.get_response_async(request)
response._handler_class = self.__class__
# Increase chunk size on file responses (ASGI servers handles low-level
# chunking).
@@ -264,7 +259,7 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
'body': chunk,
'more_body': not last,
})
response.close()
await sync_to_async(response.close)()

@classmethod
def chunk_bytes(cls, data):
@@ -1,6 +1,9 @@
import asyncio
import logging
import types

from asgiref.sync import async_to_sync, sync_to_async

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
from django.core.signals import request_finished
@@ -20,7 +23,7 @@ class BaseHandler:
_exception_middleware = None
_middleware_chain = None

def load_middleware(self):
def load_middleware(self, is_async=False):
"""
Populate middleware lists from settings.MIDDLEWARE.
@@ -30,10 +33,28 @@ def load_middleware(self):
self._template_response_middleware = []
self._exception_middleware = []

handler = convert_exception_to_response(self._get_response)
get_response = self._get_response_async if is_async else self._get_response
handler = convert_exception_to_response(get_response)
handler_is_async = is_async
for middleware_path in reversed(settings.MIDDLEWARE):
middleware = import_string(middleware_path)
middleware_can_sync = getattr(middleware, 'sync_capable', True)
middleware_can_async = getattr(middleware, 'async_capable', False)
if not middleware_can_sync and not middleware_can_async:
raise RuntimeError(
'Middleware %s must have at least one of '
'sync_capable/async_capable set to True.' % middleware_path
)
elif not handler_is_async and middleware_can_sync:
middleware_is_async = False
else:
middleware_is_async = middleware_can_async
try:
# Adapt handler, if needed.
handler = self.adapt_method_mode(
middleware_is_async, handler, handler_is_async,
debug=settings.DEBUG, name='middleware %s' % middleware_path,
)
mw_instance = middleware(handler)
except MiddlewareNotUsed as exc:
if settings.DEBUG:
@@ -49,24 +70,56 @@ def load_middleware(self):
)

if hasattr(mw_instance, 'process_view'):
self._view_middleware.insert(0, mw_instance.process_view)
self._view_middleware.insert(
0,
self.adapt_method_mode(is_async, mw_instance.process_view),
)
if hasattr(mw_instance, 'process_template_response'):
self._template_response_middleware.append(mw_instance.process_template_response)
self._template_response_middleware.append(
self.adapt_method_mode(is_async, mw_instance.process_template_response),
)
if hasattr(mw_instance, 'process_exception'):
self._exception_middleware.append(mw_instance.process_exception)
# The exception-handling stack is still always synchronous for
# now, so adapt that way.
self._exception_middleware.append(
self.adapt_method_mode(False, mw_instance.process_exception),
)

handler = convert_exception_to_response(mw_instance)
handler_is_async = middleware_is_async

# Adapt the top of the stack, if needed.
handler = self.adapt_method_mode(is_async, handler, handler_is_async)
# We only assign to this when initialization is complete as it is used
# as a flag for initialization being complete.
self._middleware_chain = handler

def make_view_atomic(self, view):
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
for db in connections.all():
if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
view = transaction.atomic(using=db.alias)(view)
return view
def adapt_method_mode(
self, is_async, method, method_is_async=None, debug=False, name=None,
):
"""
Adapt a method to be in the correct "mode":
- If is_async is False:
- Synchronous methods are left alone
- Asynchronous methods are wrapped with async_to_sync
- If is_async is True:
- Synchronous methods are wrapped with sync_to_async()
- Asynchronous methods are left alone
"""
if method_is_async is None:
method_is_async = asyncio.iscoroutinefunction(method)
if debug and not name:
name = name or 'method %s()' % method.__qualname__
if is_async:
if not method_is_async:
if debug:
logger.debug('Synchronous %s adapted.', name)
return sync_to_async(method, thread_sensitive=True)
elif method_is_async:
if debug:
logger.debug('Asynchronous %s adapted.' % name)
return async_to_sync(method)
return method

def get_response(self, request):
"""Return an HttpResponse object for the given HttpRequest."""
@@ -82,24 +135,34 @@ def get_response(self, request):
)
return response

async def get_response_async(self, request):
"""
Asynchronous version of get_response.
Funneling everything, including WSGI, into a single async
get_response() is too slow. Avoid the context switch by using
a separate async response path.
"""
# Setup default url resolver for this thread.
set_urlconf(settings.ROOT_URLCONF)
response = await self._middleware_chain(request)
response._resource_closers.append(request.close)
if response.status_code >= 400:
await sync_to_async(log_response)(
'%s: %s', response.reason_phrase, request.path,
response=response,
request=request,
)
return response

def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything that happens
inside the request/response middleware.
"""
response = None

if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()

resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
callback, callback_args, callback_kwargs = self.resolve_request(request)

# Apply view middleware
for middleware_method in self._view_middleware:
@@ -109,6 +172,9 @@ def _get_response(self, request):

if response is None:
wrapped_callback = self.make_view_atomic(callback)
# If it is an asynchronous view, run it in a subthread.
if asyncio.iscoroutinefunction(wrapped_callback):
wrapped_callback = async_to_sync(wrapped_callback)
try:
response = wrapped_callback(request, *callback_args, **callback_kwargs)
except Exception as e:
@@ -137,20 +203,89 @@ def _get_response(self, request):

return response

def process_exception_by_middleware(self, exception, request):
async def _get_response_async(self, request):
"""
Pass the exception to the exception middleware. If no middleware
return a response for this exception, raise it.
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything that happens
inside the request/response middleware.
"""
for middleware_method in self._exception_middleware:
response = middleware_method(request, exception)
response = None
callback, callback_args, callback_kwargs = self.resolve_request(request)

# Apply view middleware.
for middleware_method in self._view_middleware:
response = await middleware_method(request, callback, callback_args, callback_kwargs)
if response:
return response
raise
break

if response is None:
wrapped_callback = self.make_view_atomic(callback)
# If it is a synchronous view, run it in a subthread
if not asyncio.iscoroutinefunction(wrapped_callback):
wrapped_callback = sync_to_async(wrapped_callback, thread_sensitive=True)
try:
response = await wrapped_callback(request, *callback_args, **callback_kwargs)
except Exception as e:
response = await sync_to_async(
self.process_exception_by_middleware,
thread_sensitive=True,
)(e, request)

# Complain if the view returned None or an uncalled coroutine.
self.check_response(response, callback)

# If the response supports deferred rendering, apply template
# response middleware and then render the response
if hasattr(response, 'render') and callable(response.render):
for middleware_method in self._template_response_middleware:
response = await middleware_method(request, response)
# Complain if the template response middleware returned None or
# an uncalled coroutine.
self.check_response(
response,
middleware_method,
name='%s.process_template_response' % (
middleware_method.__self__.__class__.__name__,
)
)
try:
if asyncio.iscoroutinefunction(response.render):
response = await response.render()
else:
response = await sync_to_async(response.render, thread_sensitive=True)()
except Exception as e:
response = await sync_to_async(
self.process_exception_by_middleware,
thread_sensitive=True,
)(e, request)

# Make sure the response is not a coroutine
if asyncio.iscoroutine(response):
raise RuntimeError('Response is still a coroutine.')
return response

def resolve_request(self, request):
"""
Retrieve/set the urlconf for the request. Return the view resolved,
with its args and kwargs.
"""
# Work out the resolver.
if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()
# Resolve the view, and assign the match object back to the request.
resolver_match = resolver.resolve(request.path_info)
request.resolver_match = resolver_match
return resolver_match

def check_response(self, response, callback, name=None):
"""Raise an error if the view returned None."""
if response is not None:
"""
Raise an error if the view returned None or an uncalled coroutine.
"""
if not(response is None or asyncio.iscoroutine(response)):
return
if not name:
if isinstance(callback, types.FunctionType): # FBV
@@ -160,10 +295,41 @@ def check_response(self, response, callback, name=None):
callback.__module__,
callback.__class__.__name__,
)
raise ValueError(
"%s didn't return an HttpResponse object. It returned None "
"instead." % name
)
if response is None:
raise ValueError(
"%s didn't return an HttpResponse object. It returned None "
"instead." % name
)
elif asyncio.iscoroutine(response):
raise ValueError(
"%s didn't return an HttpResponse object. It returned an "
"unawaited coroutine instead. You may need to add an 'await' "
"into your view." % name
)

# Other utility methods.

def make_view_atomic(self, view):
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
for db in connections.all():
if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
if asyncio.iscoroutinefunction(view):
raise RuntimeError(
'You cannot use ATOMIC_REQUESTS with async views.'
)
view = transaction.atomic(using=db.alias)(view)
return view

def process_exception_by_middleware(self, exception, request):
"""
Pass the exception to the exception middleware. If no middleware
return a response for this exception, raise it.
"""
for middleware_method in self._exception_middleware:
response = middleware_method(request, exception)
if response:
return response
raise


def reset_urlconf(sender, **kwargs):

0 comments on commit fc0fa72

Please sign in to comment.
You can’t perform that action at this time.