I don't know if this is really a bug, and I needed several hours now to track it down. Please tell me if I just have some misconfig or did not understand basics of channels.
But I think that channels renders middleware - that should be run once - useless.
Using django==2.0.7, daphne==2.2.0, channels==2.1.2, Linux/Ubuntu 16.04, Firefox, just setup a simple Django project.
pip install django
pip install django-channels
django-admin startproject one
Create a middleware.py file:
from django.core.exceptions import MiddlewareNotUsed
class OneMiddleware:
def __init__(self, get_response):
"""One-time configuration and initialization of plugins."""
print('Middleware started.')
raise MiddlewareNotUsed
Now add the line
'one.middleware.OneMiddleware',
to settings.MIDDLEWARE and start it:
cd one
./manage.py migrate
./manage.py runserver
It works: At server start, 'Middleware started.' is printed, and with every F5 in the browser nothing more happens in the middleware.
If I now set up channels/routing, which works basically, the "statup-once" effect of my middleware is gone - at every reload of the page, my middleware's __init__() is called again.
Is this designed/desired functionality?
It breaks the only way you can write startup code for Django that should run once. (AppConfig.ready() is no way, and all other 'unofficial ways' neither...)
I expected this middleware to run once since MiddlewareNotUsed was raised here, as described in the Django docs.
Ah yes, I forgot about MiddlewareNotUsed when building middleware support in Channels. This needs to be added.
Thanks. What do you need to implement this? (because my app calls my routine with every page load...) Can I help to fix this?
BTW - is it necessary in a channels application to subclass channels.middleware.BaseMiddleware? Because here it is only an object without a baseclass.
No, channels doesn't really care as long as the middleware is a valid ASGI app. The BaseMiddleware is just there to provide some handy functions if you need them,
@andrewgodwin What do you propose as a fix? A complicating factor is that currently new handlers are created on every request, whereas the regular WSGIHandler is only initialised once. As I see it, the only fix is to make the handler a singleton instance somehow. See the comments below:
class ProtocolTypeRouter:
"""
Takes a mapping of protocol type names to other Application instances,
and dispatches to the right one based on protocol name (or raises an error)
"""
def __init__(self, application_mapping):
# The mapping contains class references, not instantiated objects
self.application_mapping = application_mapping
if "http" not in self.application_mapping:
# The handler is a class reference, so it is not yet initialised
self.application_mapping["http"] = AsgiHandler
def __call__(self, scope):
if scope["type"] in self.application_mapping:
# The handler is initialised here:
return self.application_mapping[scope["type"]](scope)
else:
raise ValueError("No application configured for scope type %r" % scope["type"])
@pvanagtmaal I haven't thought through it long enough to know if there's a complete fix possible, or I would have either fixed it or said here so others could :)
It may be that we just can't support MiddlewareNotUsed and instead recommend that the MIDDLEWARES setting is dynamically set at startup.
I feel that this issue has much larger implications, beyond just rendering MiddlewareNotUsed unusable.
In particular, for middleware like whitenoise which performs some fairly heavy work in its __init__, channels's approach of initializing a new AsgiHandler for each request (and thus also initializing middleware on each request) is causing serious performance problems for many users. #1121 is a good example of this.
@pvanagtmaal's suggestion would only initialize middleware once, but I'm unsure of the implications of implementing that. (I'm not super acquainted with channels yet)
Besides middleware, are there any other smart ways of performing certain actions _once_ during startup (which the middleware might be able to access the results of, down-the-line)?
Could we perhaps devise a way to load the middleware chain outside of the AsgiHandler (on defining the ProtocolTypeRouter) and simply inject it into later instantiations of the AsgiHandler? Or is that totally dumb?
@majgaard This issue is for ASGI middleware, which is different from Django middleware. ASGI middleware can be designed to overcome this limitation by just doing one-time setup outside of its inner __init__, whereas if Django middleware is slow that should be addressed in #1121 as that has an existing contract to conform to.
OMG, I opened the box of pandora here with this issue :open_mouth:.
But you are right, my initial problem was "how can I call an action ONCE during Django startup, at a defined place in time" - meaning, when certain things are loaded, like models. This seems really not easy, AppConfig.ready() is not a good way, as it is e.g. called twice during the development server start, and called from management commands as well (migrate, etc.).
I just wanted to implement that using MiddlewareNotUsed, and came across the Channels issue here.
@andrewgodwin, I'm not sure I agree with you about this issue being about ASGI middleware. The original post reads:
Now add the line
'one.middleware.OneMiddleware',
to settings.MIDDLEWARE and start it:
which implies that the middleware @nerdoc is trying to write belongs in the Django middleware chain. Regardless, I took my discussion to #1121 and suggest that we reopen that issue. I've also posted on the django-developers Google group, if anybody would like to discuss further there: https://groups.google.com/forum/#!topic/django-developers/AgZFv5y8nq4
Oh, I had read this as being for ASGI-specific middleware. But yes, let's go to #1121.
@nerdoc - can you be clear if this is Django middleware or ASGI middleware? If it's Django middleware, then we can merge the tickets
Until now I didn't even know that these are two different ones...
I use a subclass of channels.middleware.BaseMiddleware - which says in it's docstring:
Base class for implementing ASGI middleware
Does that answer your question...?
Yes, it does, thank you - it is as I thought originally, so this is a different problem.
Most helpful comment
I feel that this issue has much larger implications, beyond just rendering
MiddlewareNotUsedunusable.In particular, for middleware like whitenoise which performs some fairly heavy work in its
__init__,channels's approach of initializing a newAsgiHandlerfor each request (and thus also initializing middleware on each request) is causing serious performance problems for many users. #1121 is a good example of this.@pvanagtmaal's suggestion would only initialize middleware once, but I'm unsure of the implications of implementing that. (I'm not super acquainted with channels yet)
Besides middleware, are there any other smart ways of performing certain actions _once_ during startup (which the middleware might be able to access the results of, down-the-line)?
Could we perhaps devise a way to load the middleware chain outside of the AsgiHandler (on defining the
ProtocolTypeRouter) and simply inject it into later instantiations of the AsgiHandler? Or is that totally dumb?