Skip to content

Commit 424aada

Browse files
committed
Improve async Django support and improve docs
1 parent 8087d47 commit 424aada

File tree

7 files changed

+42
-7
lines changed

7 files changed

+42
-7
lines changed

channels/consumer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from asgiref.sync import async_to_sync
44

55
from . import DEFAULT_CHANNEL_LAYER
6-
from .db import database_sync_to_async
6+
from .db import database_sync_to_async, aclose_old_connections
77
from .exceptions import StopConsumer
88
from .layers import get_channel_layer
99
from .utils import await_many_dispatch
@@ -70,6 +70,7 @@ async def dispatch(self, message):
7070
"""
7171
handler = getattr(self, get_handler_name(message), None)
7272
if handler:
73+
await aclose_old_connections()
7374
await handler(message)
7475
else:
7576
raise ValueError("No handler for message type %s" % message["type"])

channels/db.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from asgiref.sync import SyncToAsync
1+
from asgiref.sync import SyncToAsync, sync_to_async
22
from django.db import close_old_connections
33

44

@@ -17,3 +17,7 @@ def thread_handler(self, loop, *args, **kwargs):
1717

1818
# The class is TitleCased, but we want to encourage use as a callable/decorator
1919
database_sync_to_async = DatabaseSyncToAsync
20+
21+
22+
async def aclose_old_connections():
23+
return await sync_to_async(close_old_connections)()

channels/generic/http.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from channels.consumer import AsyncConsumer
22

33
from ..exceptions import StopConsumer
4+
from ..db import aclose_old_connections
45

56

67
class AsyncHttpConsumer(AsyncConsumer):
@@ -88,4 +89,5 @@ async def http_disconnect(self, message):
8889
Let the user do their cleanup and close the consumer.
8990
"""
9091
await self.disconnect()
92+
await aclose_old_connections()
9193
raise StopConsumer()

channels/generic/websocket.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
InvalidChannelLayerError,
1010
StopConsumer,
1111
)
12+
from ..db import aclose_old_connections
1213

1314

1415
class WebsocketConsumer(SyncConsumer):
@@ -247,6 +248,7 @@ async def websocket_disconnect(self, message):
247248
"BACKEND is unconfigured or doesn't support groups"
248249
)
249250
await self.disconnect(message["code"])
251+
await aclose_old_connections()
250252
raise StopConsumer()
251253

252254
async def disconnect(self, code):

docs/topics/consumers.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ callable into an asynchronous coroutine.
112112

113113
If you want to call the Django ORM from an ``AsyncConsumer`` (or any other
114114
asynchronous code), you should use the ``database_sync_to_async`` adapter
115-
instead. See :doc:`/topics/databases` for more.
115+
or use the async versions of the methods (prefixed with ``a``, like ``aget``).
116+
See :doc:`/topics/databases` for more.
116117

117118

118119
Closing Consumers

docs/topics/databases.rst

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ code is already run in a synchronous mode and Channels will do the cleanup
1111
for you as part of the ``SyncConsumer`` code.
1212

1313
If you are writing asynchronous code, however, you will need to call
14-
database methods in a safe, synchronous context, using ``database_sync_to_async``.
14+
database methods in a safe, synchronous context, using ``database_sync_to_async``
15+
or by using the asynchronous methods prefixed with ``a`` like ``Model.objects.aget()``.
1516

1617

1718
Database Connections
@@ -26,6 +27,11 @@ Python 3.7 and below, and `min(32, os.cpu_count() + 4)` for Python 3.8+.
2627

2728
To avoid having too many threads idling in connections, you can instead rewrite your code to use async consumers and only dip into threads when you need to use Django's ORM (using ``database_sync_to_async``).
2829

30+
When using async consumers Channels will automatically call Django's ``close_old_connections`` method when a new connection is started, when a connection is closed, and whenever anything is received from the client.
31+
This mirrors Django's logic for closing old connections at the start and end of a request, to the extent possible. Connections are *not* automatically closed when sending data from a consumer since Channels has no way
32+
to determine if this is a one-off send (and connections could be closed) or a series of sends (in which closing connections would kill performance). Instead, if you have a long-lived async consumer you should
33+
periodically call ``aclose_old_connections`` (see below).
34+
2935

3036
database_sync_to_async
3137
----------------------
@@ -58,3 +64,18 @@ You can also use it as a decorator:
5864
@database_sync_to_async
5965
def get_name(self):
6066
return User.objects.all()[0].name
67+
68+
aclose_old_connections
69+
----------------------
70+
71+
``django.db.aclose_old_connections`` is an async wrapper around Django's
72+
``close_old_connections``. When using a long-lived ``AsyncConsumer`` that
73+
calls the Django ORM it is important to call this function periodically.
74+
75+
Preferrably, this function should be called before making the first query
76+
in a while. For example, it should be called if the Consumer is woken up
77+
by a channels layer event and needs to make a few ORM queries to determine
78+
what to send to the client. This function should be called *before* making
79+
those queries. Calling this function more than necessary is not necessarily
80+
a bad thing, but it does require a context switch to synchronous code and
81+
so incurs a small penalty.

docs/tutorial/part_3.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@ asynchronous consumers can provide a higher level of performance since they
1515
don't need to create additional threads when handling requests.
1616

1717
``ChatConsumer`` only uses async-native libraries (Channels and the channel layer)
18-
and in particular it does not access synchronous Django models. Therefore it can
18+
and in particular it does not access synchronous code. Therefore it can
1919
be rewritten to be asynchronous without complications.
2020

2121
.. note::
22-
Even if ``ChatConsumer`` *did* access Django models or other synchronous code it
22+
Even if ``ChatConsumer`` *did* access Django models or synchronous code it
2323
would still be possible to rewrite it as asynchronous. Utilities like
2424
:ref:`asgiref.sync.sync_to_async <sync_to_async>` and
2525
:doc:`channels.db.database_sync_to_async </topics/databases>` can be
2626
used to call synchronous code from an asynchronous consumer. The performance
27-
gains however would be less than if it only used async-native libraries.
27+
gains however would be less than if it only used async-native libraries. Django
28+
models include methods prefixed with ``a`` that can be used safely from async
29+
contexts, provided that
30+
:doc:`channels.db.aclose_old_connections </topics/databases>` is called
31+
occasionally.
2832

2933
Let's rewrite ``ChatConsumer`` to be asynchronous.
3034
Put the following code in ``chat/consumers.py``:

0 commit comments

Comments
 (0)