Changing how Channels/ASGI receives messages

231 views
Skip to first unread message

Andrew Godwin

unread,
Mar 27, 2017, 9:51:20 PM3/27/17
to Django developers (Contributions to Django itself)
Hi all,

I wanted to ask your feedback on a proposed change I want to make to the ASGI spec for channel layers.

In particular, my goal is to make it so that Daphne only has to receive() on a single channel at any time, massively simplifying the work it takes to poll for new messages and the overhead that switching channel sets all the time caused.

The way I want to do this is to redefine process-local channels - those with a ! in them - so that they are written like this:

    http.response.PROCESSID!LOCALID

Then, change the semantics of receive() so that you only call

    channel_layer.receive(["http.response.PROCESSID!"])

And this then receives any message sent to any channel with that prefix. The channel name returned from receive() is still the full name as sent by the client; it's basically acting as a very limited wildcard.

This will allow Daphne (and any other server) to listen on just a single process-local channel, with a random part made by the process at boot (e.g. "http.response.A34GF3!"), and then internally dispatch messages as they come in to the appropriate client socket. It also means that the client API - the one seen by end-developers and the Channels library itself - does not change at all, as the reply_channel can still be set to something like "http.response.A34GF3!K89C3", and everything will flow correctly.

It's better than full wildcard support as that may be very inefficient to implement depending on the server; the limitation means that implementations can just deliver into queues based on the non-local part and stuff the "full" channel name into the message storage area, and unpack it again on receive.

The potential benefits to efficiency are hard to understate; the current definition of process-local channels was meant to allow channel layers to do something like this internally but it still needed server code to swap out the list of things it was listening on every receive() call. By removing this abstraction, servers can be written with a single tight loop around a fixed set of channel names, and we can block way more efficiently on servers like Redis.

I want to know if anyone has any feedback on this, especially from the "potential problems" department; if not, I'm going to try and get this out before DjangoCon Europe.

Thanks!

Andrew

Florian Apolloner

unread,
Mar 28, 2017, 7:56:22 AM3/28/17
to Django developers (Contributions to Django itself)
That sounds like a massive improvement and goes in line with the issues I've observed.

Artem Malyshev

unread,
Mar 28, 2017, 9:27:39 AM3/28/17
to Django developers (Contributions to Django itself)
Hi everyone,

It can be huge improvement for asgi_rabbitmq layer. But I'm asking
you to keep this change in the experimental branch before I implement
it for RabbitMQ layer. More questions can came up in progress.

For me it is still unclear how to implement `channels_capacity` option
for different reply channels in this multiplexing. If all reply
channels for current process will have it's obvious.

Regards, Artem.

Andrew Godwin

unread,
Mar 28, 2017, 12:47:21 PM3/28/17
to Django developers (Contributions to Django itself)
On Tue, Mar 28, 2017 at 6:27 AM, Artem Malyshev <proof...@gmail.com> wrote:
Hi everyone,

It can be huge improvement for asgi_rabbitmq layer.  But I'm asking
you to keep this change in the experimental branch before I implement
it for RabbitMQ layer.  More questions can came up in progress.

I've actually already implemented it on all of the projects on the master branch last night to see how hard it was (it was not hard at all), but I'll hold off releasing those changes for a bit.
 

For me it is still unclear how to implement `channels_capacity` option
for different reply channels in this multiplexing.  If all reply
channels for current process will have it's obvious.

Would it be easier if we said that the capacity applied only to the overall process channel, e.g. to "http.response.ABCDEF!", not to the local part as well? I think this makes more sense anyway, and that's how asgi_redis will have ended up after my change.

Andrew
 

Artem Malyshev

unread,
Mar 28, 2017, 12:54:52 PM3/28/17
to Django developers (Contributions to Django itself)
Yes, I saw your changes after I wrote my previous email.

Global capacity for process sounds reasonable. Probably we should state it in spec.

Regards, Artem.

Andrew Godwin

unread,
Mar 28, 2017, 1:36:26 PM3/28/17
to Django developers (Contributions to Django itself)
I have added that in to the spec: http://channels.readthedocs.io/en/latest/asgi.html#capacity

Andrew


Regards, Artem.

--
You received this message because you are subscribed to the Google Groups "Django developers  (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-developers+unsubscribe@googlegroups.com.
To post to this group, send email to django-developers@googlegroups.com.
Visit this group at https://groups.google.com/group/django-developers.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-developers/a4af7e17-5ebe-41c5-b30d-f68110d348b4%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Oskar Hahn

unread,
Mar 28, 2017, 1:39:49 PM3/28/17
to django-d...@googlegroups.com
Hi,

I am not sure, if I understand your proposal correctly. Do you mean, the
asgi server has to listen only to one channel or do you mean it has to
listen to one channel of any channel-type?

So with redis, do I have to call

"BLPOP LOCALID TIMEOUT"

or do I have to call

"BLPOP http.response.PROCESSID! TIMEOUT" and
"BLPOP websocket.send.PROCESSID! TIMEOUT"

I think you mean the second one, but then your goal, that daphne has
only to listen "on a single channel at any time" seams not be fulfilled.

I would like a solution, were the asgi server really listens only to one
channel. I don't like the idea, that there is a fixed set of channel
names. This seems not flexible.


Another thing:

Currently the usual case looks like this:

Time | ASGI Application | ASGI Server |
----------------------------------------------
1 | | receive("foo!123") |
2 | send("foo!123") | |

First, the asgi server listens to a specific channel, for example
http.response.RANDOMPART!CLIENTID. Then the Django app sends something
to this channel and the asgi server gets it.

But what should happen when the django app sends to a channel and the
asgi server starts to listen on this channel at a later time:

Time | ASGI Application | ASGI Server |
----------------------------------------------
1 | send("foo!123") | |
2 | | receive("foo!123") |

With the current implementation the messages remains in the asgi-backend
until it expires. So the ASGI Server can fetch it later.

With your proposal this could change. The ASGI-Server will get the
message from the asgi-backend, as soon, as it was send by the
application. So if nobody has called receive() for this channel, it
either has to be dropped or saved.

Do you have a recommendation what to do with a message, that can not be
dispatched to a client socket? This should not happen so often.

Oskar
> --
> You received this message because you are subscribed to the Google
> Groups "Django developers (Contributions to Django itself)" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to django-develop...@googlegroups.com
> <mailto:django-develop...@googlegroups.com>.
> To post to this group, send email to django-d...@googlegroups.com
> <mailto:django-d...@googlegroups.com>.
> Visit this group at https://groups.google.com/group/django-developers.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/django-developers/CAFwN1urL6dj-X6fF%2BDAPhz5jJHmYXTX6GV0M7Y5iGxSOgog8rA%40mail.gmail.com
> <https://groups.google.com/d/msgid/django-developers/CAFwN1urL6dj-X6fF%2BDAPhz5jJHmYXTX6GV0M7Y5iGxSOgog8rA%40mail.gmail.com?utm_medium=email&utm_source=footer>.
signature.asc

Andrew Godwin

unread,
Mar 28, 2017, 1:49:49 PM3/28/17
to Django developers (Contributions to Django itself)
On Tue, Mar 28, 2017 at 10:39 AM, Oskar Hahn <ma...@oshahn.de> wrote:
Hi,

I am not sure, if I understand your proposal correctly. Do you mean, the
asgi server has to listen only to one channel or do you mean it has to
listen to one channel of any channel-type?

So with redis, do I have to call

"BLPOP LOCALID TIMEOUT"

or do I have to call

"BLPOP http.response.PROCESSID! TIMEOUT" and
"BLPOP websocket.send.PROCESSID! TIMEOUT"

I think you mean the second one, but then your goal, that daphne has
only to listen "on a single channel at any time" seams not be fulfilled.

I would like a solution, were the asgi server really listens only to one
channel. I don't like the idea, that there is a fixed set of channel
names. This seems not flexible.

I mean that the server now has the option to go down to one or two channels. BLPOP supports multiple channel names, so the two-channel case can be done as:

BLPOP http.response.PROCESSID! websocket.send.PROCESSID! TIMEOUT

However, as this would play oddly with sharding, I have instead modified Daphne to use a single `daphne.response.PROCESSID!` channel for both HTTP and WebSocket replies - the name of the reply_channel is entirely up to the server, after all.

 


Another thing:

Currently the usual case looks like this:

Time | ASGI Application | ASGI Server        |
----------------------------------------------
1    |                  | receive("foo!123") |
2    | send("foo!123")  |                    |

First, the asgi server listens to a specific channel, for example
http.response.RANDOMPART!CLIENTID. Then the Django app sends something
to this channel and the asgi server gets it.

But what should happen when the django app sends to a channel and the
asgi server starts to listen on this channel at a later time:

Time | ASGI Application | ASGI Server        |
----------------------------------------------
1    | send("foo!123")  |                    |
2    |                  | receive("foo!123") |

With the current implementation the messages remains in the asgi-backend
until it expires. So the ASGI Server can fetch it later.

With your proposal this could change. The ASGI-Server will get the
message from the asgi-backend, as soon, as it was send by the
application. So if nobody has called receive() for this channel, it
either has to be dropped or saved.

Do you have a recommendation what to do with a message, that can not be
dispatched to a client socket? This should not happen so often.

Well, given that the reply channel name is generated by the server itself, it should not be possible that the server does not know about it when the client sends it, as the client had to get it from somewhere. If a message is received for a non-existent reply channel, which is possible if, say, the socket closed before the response could be read, the server will log this and continue on.

Andrew 
Reply all
Reply to author
Forward
0 new messages