Hi there!
We’ve been running into a recurring, confusing issue when using Django in conjunction with gunicorn (gevent) and channels. I have simplified the setup to this very simple code that is causing a problem:
View:
from django.views.generic import View
from django.http import HttpResponse
from channels.layers import get_channel_layer
def send_message():
layer = get_channel_layer()
async_to_sync(layer.group_send)("test_group", "test_message")
class HomeView(View):
def get(self, request):
# Send a message via channels
send_message()
return HttpResponse("")
So: a simple view that is just pushing a message to a channel layer every time it is hit.
We then run Django with:
gunicorn "myproject.wsgi:application" --bind "0.0.0.0" --workers 4 --worker-class gevent
Now, if we hit the HomeView with just a few dozen concurrent requests, we start running into this error when Django tries to query the database (in this case from the session middleware):
File "/usr/local/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1880, in execute_sql
with self.connection.cursor() as cursor:
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/utils/asyncio.py", line 24, in inner
raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
I have tried using force_new_loop=True in async_to_sync, and that makes no difference. So something in this example seems to be initialising an event loop in the main thread, which then causes the async_unsafe check to raise an error.
As far as I can tell, nothing in here should be initialising the event loop other than the call to async_to_sync, so I’m very confused about what is going on. Is this some interaction between gunicorn/gevent and Django? What is the correct way to push to a channel layer with this setup without creating an async-unsafe context in the main thread?