Pipfile
:
[packages]
Django = "==3.1.3"
asgiref = "==3.2.10"
channels = "==3.0.2"
daphne = "==3.0.1"
(Note: asgiref 3.3.10 was also tried)
Code / Minimal example
I started a completely new Django + Django channels project to verify I can still see the same behaviour.
views.py
:
def view_1(request):
for i in range(10):
time.sleep(1)
print(request, i)
return HttpResponse(b'Hello World #1!')
def view_2(request):
return HttpResponse(b'Hello World #2!')
asgi.py
:
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from django.urls import re_path
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'channels_test.settings')
application = ProtocolTypeRouter({
'http': URLRouter([
re_path('', get_asgi_application())
])
})
urls.py
:
urlpatterns = [
path('admin/', admin.site.urls),
path('view1/', views.view_1),
path('view2/', views.view_2),
]
I have two Django views. View1
and View2
. Both are synchronous. As you can see, View1
sleeps for 10 seconds before returning a response. Calls to View2 respond instantaneously.
How you're running Channels (runserver? daphne/runworker? Nginx/Apache in front?)
Issue is seen in both runserver and also when running a single daphne worker. Nginx is serving the requests.
What Happened
If I open up a browser and go to localhost/view1, I can see in the console the printouts of the sleep for 10 seconds. If, while view1 is still processing, I go to localhost/view2, the page does not respond until view1 has finished processing.
What I expected
I should not expect View2 to have to wait for View1 to finish before returning a response, the requests should run concurrently. If I revert back to channels 2.4.0 with the following pip setup:
Pipfile
:
[packages]
Django = "==3.1.3"
asgiref = "==3.2.10"
channels = "==2.4.0"
daphne = "==2.5.0"
and the following asgi.py
:
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.routing import AsgiHandler
from django.urls import re_path
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'channels_test.settings')
import django
django.setup()
application = ProtocolTypeRouter({
'http': URLRouter([
re_path('', AsgiHandler)
])
})
I no longer see the behaviour of the ticket. As I did not expect a regression in behaviour for synchronous views after upgrading django / channels, I expected to see this same older behaviour after upgrading channels.
Workarounds
-
Convert the synchronous views to asynchronous views. This is not feasible for me as my entire project uses django rest framework which does not support asynchronous views (as Django does not provide async generic views).
-
Reverting back to Channels 2.X / Daphne 2.X.
Investigation / Notes
-
While I know Django introduced async views, I did not expect a regression in behaviour for existing uses of synchronous views with ASGI. I did not see documentation indicating this behaviour should be expected.
-
For my project, all APIs sent are now processed sequentially resulting in a very unresponsive front-end.
-
I am certain this is down to the use of Django's ASGIHandler
and its use of thread_sensitive=True
in sync_to_async
. Synchronous views are all placed in the same thread with thread_sensitive=True
, so I'm assuming this is the cause of the issue - and wasn't an issue with channel's own version of ASGIHandler
. If I access django.core.handlers.base
and modify thread_sensitive
of _get_response_async
to False
, the view responds instantly, like normal:
Line 228
of django.core.handlers.base
if not asyncio.iscoroutinefunction(wrapped_callback):
wrapped_callback = sync_to_async(wrapped_callback, thread_sensitive=False)
- I wasn't sure whether to report as a Channels or a Django issue. As using Django's
get_asgi_application
is part of the recommended way of upgrading channels from 2.X
to 3.X
, and this is all part of the Django ecosystem, it felt more appropriate here.