Skip to content

Commit 8d90b07

Browse files
authored
Improve async Django support and improve docs (#2090)
1 parent aa91c28 commit 8d90b07

11 files changed

+55
-7
lines changed

channels/consumer.py

+2-1
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 aclose_old_connections, database_sync_to_async
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

+5-1
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

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from channels.consumer import AsyncConsumer
22

3+
from ..db import aclose_old_connections
34
from ..exceptions import StopConsumer
45

56

@@ -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

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from asgiref.sync import async_to_sync
44

55
from ..consumer import AsyncConsumer, SyncConsumer
6+
from ..db import aclose_old_connections
67
from ..exceptions import (
78
AcceptConnection,
89
DenyConnection,
@@ -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

+2-1
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

+22-1
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

+7-3
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``:

tests/security/test_websocket.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from channels.testing import WebsocketCommunicator
66

77

8+
@pytest.mark.django_db(transaction=True)
89
@pytest.mark.asyncio
910
async def test_origin_validator():
1011
"""

tests/test_generic_http.py

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from channels.testing import HttpCommunicator
99

1010

11+
@pytest.mark.django_db(transaction=True)
1112
@pytest.mark.asyncio
1213
async def test_async_http_consumer():
1314
"""
@@ -38,6 +39,7 @@ async def handle(self, body):
3839
assert response["headers"] == [(b"Content-Type", b"application/json")]
3940

4041

42+
@pytest.mark.django_db(transaction=True)
4143
@pytest.mark.asyncio
4244
async def test_error():
4345
class TestConsumer(AsyncHttpConsumer):
@@ -51,6 +53,7 @@ async def handle(self, body):
5153
assert str(excinfo.value) == "Error correctly raised"
5254

5355

56+
@pytest.mark.django_db(transaction=True)
5457
@pytest.mark.asyncio
5558
async def test_per_scope_consumers():
5659
"""
@@ -87,6 +90,7 @@ async def handle(self, body):
8790
assert response["body"] != second_response["body"]
8891

8992

93+
@pytest.mark.django_db(transaction=True)
9094
@pytest.mark.asyncio
9195
async def test_async_http_consumer_future():
9296
"""

tests/test_generic_websocket.py

+7
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def receive(self, text_data=None, bytes_data=None):
154154
assert channel_layer.groups == {}
155155

156156

157+
@pytest.mark.django_db(transaction=True)
157158
@pytest.mark.asyncio
158159
async def test_async_websocket_consumer():
159160
"""
@@ -195,6 +196,7 @@ async def disconnect(self, code):
195196
assert "disconnected" in results
196197

197198

199+
@pytest.mark.django_db(transaction=True)
198200
@pytest.mark.asyncio
199201
async def test_async_websocket_consumer_subprotocol():
200202
"""
@@ -217,6 +219,7 @@ async def connect(self):
217219
assert subprotocol == "subprotocol2"
218220

219221

222+
@pytest.mark.django_db(transaction=True)
220223
@pytest.mark.asyncio
221224
async def test_async_websocket_consumer_groups():
222225
"""
@@ -253,6 +256,7 @@ async def receive(self, text_data=None, bytes_data=None):
253256
assert channel_layer.groups == {}
254257

255258

259+
@pytest.mark.django_db(transaction=True)
256260
@pytest.mark.asyncio
257261
async def test_async_websocket_consumer_specific_channel_layer():
258262
"""
@@ -323,6 +327,7 @@ def receive_json(self, data=None):
323327
await communicator.wait()
324328

325329

330+
@pytest.mark.django_db(transaction=True)
326331
@pytest.mark.asyncio
327332
async def test_async_json_websocket_consumer():
328333
"""
@@ -355,6 +360,7 @@ async def receive_json(self, data=None):
355360
await communicator.wait()
356361

357362

363+
@pytest.mark.django_db(transaction=True)
358364
@pytest.mark.asyncio
359365
async def test_block_underscored_type_function_call():
360366
"""
@@ -390,6 +396,7 @@ async def _my_private_handler(self, _):
390396
await communicator.receive_from()
391397

392398

399+
@pytest.mark.django_db(transaction=True)
393400
@pytest.mark.asyncio
394401
async def test_block_leading_dot_type_function_call():
395402
"""

tests/test_testing.py

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ async def http_request(self, event):
2323
await self.send({"type": "http.response.body", "body": b"test response"})
2424

2525

26+
@pytest.mark.django_db(transaction=True)
2627
@pytest.mark.asyncio
2728
async def test_http_communicator():
2829
"""

0 commit comments

Comments
 (0)