From 60f0680ec21102d73c4a2044ed3a7d11fa9c6455 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 25 Jun 2015 21:33:22 -0700 Subject: [PATCH] Update docs a bit more --- docs/concepts.rst | 91 +++++++++++++++++++++++++++++++++++++--- docs/getting-started.rst | 47 ++++++++++++++++++++- 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/docs/concepts.rst b/docs/concepts.rst index 3171b01..e4d0edd 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -26,7 +26,7 @@ What is a channel? The core of Channels is, unsurprisingly, a datastructure called a *channel*. What is a channel? It is an *ordered*, *first-in first-out queue* with -*at-most-once delivery* to *only one listener at a time*. +*message expiry* and *at-most-once delivery* to *only one listener at a time*. You can think of it as analagous to a task queue - messages are put onto the channel by *producers*, and then given to just one of the *consumers* @@ -147,9 +147,9 @@ the message - but response channels would have to have their messages sent to the channel server they're listening on. For this reason, Channels treats these as two different *channel types*, and -denotes a response channel by having the first character of the channel name -be the character ``!`` - e.g. ``!django.wsgi.response.f5G3fE21f``. Normal -channels have no special prefix, but along with the rest of the response +denotes a *response channel* by having the first character of the channel name +be the character ``!`` - e.g. ``!django.wsgi.response.f5G3fE21f``. *Normal +channels* have no special prefix, but along with the rest of the response channel name, they must contain only the characters ``a-z A-Z 0-9 - _``, and be less than 200 characters long. @@ -167,6 +167,85 @@ keep track of which response channels of those you wish to send to. Say I had a live blog where I wanted to push out updates whenever a new post is saved, I would register a handler for the ``post_save`` signal and keep a -set of channels to send updates to:: +set of channels (here, using Redis) to send updates to:: - (todo) + + redis_conn = redis.Redis("localhost", 6379) + + @receiver(post_save, sender=BlogUpdate) + def send_update(sender, instance, **kwargs): + # Loop through all response channels and send the update + for send_channel in redis_conn.smembers("readers"): + Channel(send_channel).send( + id=instance.id, + content=instance.content, + ) + + @Channel.consumer("django.websocket.connect") + def ws_connect(path, send_channel, **kwargs): + # Add to reader set + redis_conn.sadd("readers", send_channel) + +While this will work, there's a small problem - we never remove people from +the ``readers`` set when they disconnect. We could add a consumer that +listens to ``django.websocket.disconnect`` to do that, but we'd also need to +have some kind of expiry in case an interface server is forced to quit or +loses power before it can send disconnect signals - your code will never +see any disconnect notification but the response channel is completely +invalid and messages you send there will never get consumed and just expire. + +Because the basic design of channels is stateless, the channel server has no +concept of "closing" a channel if an interface server goes away - after all, +channels are meant to hold messages until a consumer comes along (and some +types of interface server, e.g. an SMS gateway, could theoretically serve +any client from any interface server). + +That means that we need to follow a keepalive model, where the interface server +(or, if you want even better accuracy, the client browser/connection) sends +a periodic message saying it's still connected (though only for persistent +connection types like WebSockets; normal HTTP doesn't need this as it won't +stay connected for more than its own timeout). + +Now, we could go back into our example above and add an expiring set and keep +track of expiry times and so forth, but this is such a common pattern that +we don't need to; Channels has it built in, as a feature called Groups:: + + @receiver(post_save, sender=BlogUpdate) + def send_update(sender, instance, **kwargs): + Group("liveblog").send( + id=instance.id, + content=instance.content, + ) + + @Channel.consumer("django.websocket.connect") + @Channel.consumer("django.websocket.keepalive") + def ws_connect(path, send_channel, **kwargs): + # Add to reader group + Group("liveblog").add(send_channel) + +Not only do groups have their own ``send()`` method (which backends can provide +an efficient implementation of), they also automatically manage expiry of +the group members. You'll have to re-call ``Group.add()`` every so often to +keep existing members from expiring, but that's easy, and can be done in the +same handler for both ``connect`` and ``keepalive``, as you can see above. + +Groups are generally only useful for response channels (ones starting with +the character ``!``), as these are unique-per-client. + +Next Steps +---------- + +That's the high-level overview of channels and groups, and how you should +starting thinking about them - remember, Django provides some channels +but you're free to make and consume your own, and all channels are +network-transparent. + +One thing channels are not, however, is guaranteed delivery. If you want tasks +you're sure will complete, use a system designed for this with retries and +persistence like Celery, or you'll need to make a management command that +checks for completion and re-submits a message to the channel if nothing +is completed (rolling your own retry logic, essentially). + +We'll cover more about what kind of tasks fit well into Channels in the rest +of the documentation, but for now, let's progress to :doc:`getting-started` +and writing some code. diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 059f831..67054cc 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -1,6 +1,49 @@ Getting Started =============== -(If you haven't yet, make sure you :doc:`install Channels ` -and read up on :doc:`the concepts behind Channels `) +(If you haven't yet, make sure you :doc:`install Channels `) +Now, let's get to writing some consumers. If you've not read it already, +you should read :doc:`concepts`, as it covers the basic description of what +channels and groups are, and lays out some of the important implementation +patterns and caveats. + +First Consumers +--------------- + +Now, by default, Django will run things through Channels but it will also +tie in the URL router and view subsystem to the default ``django.wsgi.request`` +channel if you don't provide another consumer that listens to it - remember, +only one consumer can listen to any given channel. + +As a very basic example, let's write a consumer that overrides the built-in +handling and handles every HTTP request directly. Make a new project, a new +app, and put this in a ``consumers.py`` file in the app:: + + from channels import Channel + from django.http import HttpResponse + + @Channel.consumer("django.wsgi.request") + def http_consumer(response_channel, path, **kwargs): + response = HttpResponse("Hello world! You asked for %s" % path) + Channel(response_channel).send(response.channel_encode()) + +The most important thing to note here is that, because things we send in +messages must be JSON-serialisable, the request and response messages +are in a key-value format. There are ``channel_decode()`` and +``channel_encode()`` methods on both Django's request and response classes, +but here we just take two of the request variables directly as keyword +arguments for simplicity. + +If you start up ``python manage.py runserver`` and go to +``http://localhost:8000``, you'll see that, rather than a default Django page, +you get the Hello World response, so things are working. If you don't see +a response, check you :doc:`installed Channels correctly `. + +Now, that's not very exciting - raw HTTP responses are something Django can +do any time. Let's try some WebSockets! + +Delete that consumer from above - we'll need the normal Django view layer to +serve templates later - and make this WebSocket consumer instead:: + + # todo