-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
Fixed #31224 -- Added support for asynchronous views and middleware. #11650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
is it too late to consider trio? for class-based async views, can we get inspiration from django-vanilla-views? what about making new async views instead of current class based views? |
@auvipy Unfortunately, while I like trio, it has a much smaller library ecosystem and so pragmatically we can't use it. My hope is eventually to make it possible to use in some situations. Some more class-based views discussion is over on the forum (https://forum.djangoproject.com/c/internals/async) - while new async views is cleanest, it does mean having two versions of everything to maintain, which isn't great. |
9eb4440
to
630f691
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Greetings, @andrewgodwin.
More recently, I've become significantly involved with the development of asyncio. I'm by no means as experienced as Yury or Andrew, but I am quite familiar with the internals of asyncio and the low-level API.
I'm not familiar with the internals of Django and did not get a chance to look this over extensively, but I do have some feedback regarding the usage of asycnio.iscoroutine()
. Specifically, asyncio.iscoroutine()
supports the legacy generator coroutines, which are currently deprecated and scheduled for removal in Python 3.10.
As a result, I would recommend using inspect.iscoroutine()
instead. With inspect.iscoroutine()
, it only returns True
if the coroutine was declared with the async def
syntax. With asyncio.iscoroutine()
, it will return True
for the legacy generator coroutines declared with the @asyncio.coroutine
decorator in addition to the async def
coroutines.
This may not be a concern if you intend to support the legacy generator coroutines. I just wanted to provide some insight in case you weren't aware of the difference and don't intend to provide legacy support for the older coroutines. It's definitely not a particularly easy to notice difference if you were used to using asyncio.iscoroutine()
in the past. At the moment, I'm not certain if there are plans to change asyncio.iscoroutine()
to remove support for the legacy coroutines or deprecate the function entirely, I'll have to ask the others.
References
asyncio.iscoroutine()
:
Docs: https://docs.python.org/3/library/asyncio-task.html#asyncio.iscoroutine
Source code: https://github.com/python/cpython/blob/f900064ac4b35226caad7502abc8a7e64f1c0e9d/Lib/asyncio/coroutines.py#L164
inspect.iscoroutine()
:
Docs: https://docs.python.org/3/library/inspect.html#inspect.iscoroutine
Source code: https://github.com/python/cpython/blob/f900064ac4b35226caad7502abc8a7e64f1c0e9d/Lib/inspect.py#L189
@aeros We use the By my reading, the |
@andrewgodwin That's correct. I mainly wanted to make sure that it was intentional and it's clear that doing so will indirectly support the older generator coroutines that are being deprecated in Python 3.10 (unless |
1df9d06
to
a22d053
Compare
d419171
to
e6035d3
Compare
Given that this branch now passes all tests and shows no drop in sync performance in my testing, I consider it ready for review. I've also added relevant docs. (I did not squash the commits down as I want to either do that with github's squash-and-merge, or wait until all requested changes are through and we're ready to go) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andrewgodwin, this is lovely work. Clear, clean, and really easy to follow (surprisingly so - because the base handler has always been a bit complex; the refactoring you needed to do actually makes things easier to follow, for me at least). Thank you! I'm super-excited to give this a serious try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @andrewgodwin.
Thanks for this. Really good.
...until we're a release or two in and there's more community knowledge about async views and how to write them
I like the docs how we are, for the stage we're at. I'll look again but, I think they're pitched about right.
(Very readable as ever 👍)
My main concern at this point is the test coverage. This is the heart of the framework and, there are a (small) number of lines in the changes here that aren't hit by tests. They're ≈all around sync/async, in say load_middleware()
, check_none_response()
, resolve_request()
, etc. I think we need to ensure each branch is at least exercised.
Running coverage (where I always forget --parallel=
, gaps (here) are in handlers/base.py
, handlers/exception.py
, handlers/wsgi.py
(but see below), and utils/decorators.py
.
It doesn't look like a million cases are needed, but, for example, there's no middleware testing the ..must have at least one of...
error. And so on.
Good point about the testing - I always tend to forget that late in a project (especially as I was mostly performance-testing this with a separate suite). I'll try to get some extra tests in today or over the weekend and get that coverage up. |
@carltongibson I've added more handler tests as well as the necessary |
Super @andrewgodwin! I saw the commit come in and had to laugh. Great work. Will look again tomorrow. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @andrewgodwin. Getting there with this... 😅
There's a weirdness with using @async_to_sync
on individual test methods.
If I run the test case class, no problem:
(django) django/tests (pr/11650)$ ./runtests.py handlers.tests.AsyncHandlerRequestTests
Testing against Django installed in 'django/django' with up to 4 processes
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.113s
OK
But running just a single test fails:
(django) django/tests (pr/11650)$ ./runtests.py handlers.tests.AsyncHandlerRequestTests.test_unawaited_response
Testing against Django installed in 'django/django' with up to 4 processes
Traceback (most recent call last):
...[snip]...
File "/Users/carlton/ve/django/lib/python3.7/site-packages/asgiref/sync.py", line 156, in main_wrap
result = await self.awaitable(*args, **kwargs)
File "django/tests/handlers/tests.py", line 284, in test_unawaited_response
with self.assertRaisesMessage(ValueError, msg):
AttributeError: 'NoneType' object has no attribute 'assertRaisesMessage'
Looking at this in the debugger, moving up to the async_to_sync wrapper, we have:
(Pdb) u
> /Users/carlton/ve/django/lib/python3.7/site-packages/asgiref/sync.py(156)main_wrap()
-> result = await self.awaitable(*args, **kwargs)
(Pdb) l
151 try:
152 raise exc_info[1]
153 except:
154 result = await self.awaitable(*args, **kwargs)
155 else:
156 -> result = await self.awaitable(*args, **kwargs)
157 except Exception as e:
158 call_result.set_exception(e)
159 else:
160 call_result.set_result(result)
161 finally:
(Pdb) self
<asgiref.sync.AsyncToSync object at 0x1105a9e80>
(Pdb) self.awaitable
<function AsyncHandlerRequestTests.test_sync_view at 0x1105c0ae8>
If I wrap with method_decorator, e.g.
@method_decorator(async_to_sync)
async def test_sync_view(self):
...
The tests are correct, the test's self
is correct, and looking in the debugger
we have a bound method:
(django) django/tests (pr/11650)$ ./runtests.py handlers.tests.AsyncHandlerRequestTests.test_sync_view --parallel=1
Testing against Django installed in 'django/django'
System check identified no issues (0 silenced).
> django/tests/handlers/tests.py(256)test_sync_view()
-> response = await self.async_client.get('/regular/')
(Pdb) self
<handlers.tests.AsyncHandlerRequestTests testMethod=test_sync_view>
(Pdb) c
.
----------------------------------------------------------------------
Ran 1 test in 7.930s
OK
(django) ~/Documents/Django-Stack/django/tests (pr/11650)$ ./runtests.py handlers.tests.AsyncHandlerRequestTests.test_sync_view --parallel=1
...
(Pdb) u
> /Users/carlton/ve/django/lib/python3.7/site-packages/asgiref/sync.py(156)main_wrap()
-> result = await self.awaitable(*args, **kwargs)
(Pdb) self.awaitable
functools.partial(<bound method AsyncHandlerRequestTests.test_sync_view of <handlers.tests.AsyncHandlerRequestTests testMethod=test_sync_view>>)
(Pdb) c
.
----------------------------------------------------------------------
Ran 1 test in 229.461s
OK
Not (at this moment) 100% sure why it works running the whole test case because
the awaitable is still the function, rather than the bound method...
(django) django/tests (pr/11650)$ ./runtests.py handlers.tests.AsyncHandlerRequestTests --parallel=1
Testing against Django installed in 'django/django'
System check identified no issues (0 silenced).
...> django/tests/handlers/tests.py(257)test_sync_view()
-> response = await self.async_client.get('/regular/')
(Pdb) u
> /Users/carlton/ve/django/lib/python3.7/site-packages/asgiref/sync.py(156)main_wrap()
-> result = await self.awaitable(*args, **kwargs)
(Pdb) self.awaitable
<function AsyncHandlerRequestTests.test_sync_view at 0x10b660a60>
(Pdb) c
..
----------------------------------------------------------------------
Ran 5 tests in 21.410s
OK
...so I need to dig a little more.
However...
What do you think about this case? (In the testing docs, should we be mentioning
that you probably should be using method_decorator?)
@carltongibson Ah, interesting! I only ever run the test suites rather than the individual methods, so I never ran into this. Let me go dive in and see if I can figure out a way to have Django do this automatically; I had considered making the test runner just auto-apply |
Hey @andrewgodwin Super. Thanks. Whilst you're looking... My other (currently proto-) question is, Can we reduce the number of
Am still playing with it, but thought I'd mention it early since you're here. 🙂 Other than that, this is looking good I think. (Time to ask @felixxm to get out the fine-toothed comb, before another fine read of the docs...) Thanks for the super effort. |
@carltongibson I don't think I can stop |
I think that's allowed:
Runs as
Nothing seemed to break when I was experimenting earlier. I see you rebased. Failures are from 4e8d6a1 |
@andrewgodwin In the last commit aff07dc527f9ae6b91c68ed741258fbd15d74045 I tried to use |
@carltongibson That fixup looks good to me - thanks for doing all the Sphinx linking that I often forget! @felixxm That looks like a sensible change to me, nice way of reducing the scope duplication. |
I moved documentation of async adapter functions to a separate PR #12562. |
@andrewgodwin I think |
I push small changes:
|
I added a small section about |
fe0ddbc
to
97ead93
Compare
@andrewgodwin Can you take a look at our recent changes? |
I just pushed 40ca60c10f09edc0a24acf756c8daf471f8b82de, which adds an:
... at the end of the FileResponse test case Previously we had a:
This was caused by the We just need to make sure the app has a chance to finish in these cases. |
Yup, all these edits look good to me. Thanks to both of you! |
Cheers @andrewgodwin. Thanks for the confirmation. I think we're there 🤞. @felixxm can press the button. 😀 💃⛵️🎇 |
This implements support for asynchronous views, asynchronous tests, asynchronous middleware, and an asynchronous test client.
Thanks y'all 🥇 🚀 🎊 |
Thanks so much for all your hard work reviewing! Now, let's take a look at that ORM... :) |
Thank you for this! |
ticket-31224
This implements the ability to have asynchronous views in Django, by simply declaring a view as
async def
.Under the hood, it rewrites the BaseHandler to be natively async and makes the WSGI request path run an async loop in-place for every request in order to service this. The ASGI request path down to a view is instead intended to be natively asynchronous all the way down.
This is still a work in progress, as it currently has several potential issues:
I'm putting it up as a draft PR so people have a place to easily see it and review the code so far.