It's been a little while since I've written a blog post about Channels - this is partially because I've been taking some time off for the conflux of conferences, work and light medical issues that have cropped up, but things have been slowly rattling away in the background and I'm now back working at a decent pace on the changes.
A lot of this has been figuring out where to take Channels 2, and struggling with my own personal hangups - specifically, being divided between wanting to make the upgrade as backwards-compatible and painless as possible but also feeling like I want to fix some major design issues in the process.
A big part of this has been Python 3 and native async syntax, as well - for a long time, I wanted to support Python 2, but as time as moved on and Django moves away from supporting it entirely in Django 2.0, and Twisted's asyncio support is mostly mature, it's time to drop that handle as well.
The result is a Channels that is really quite substantially different in terms of underlying architecture and design, but which ends up with a very similar interface to the end developer. Let's take a look at some of those changes.
Running code in-process
Perhaps the biggest change of all is that Channels 2 runs your handling code in process with the HTTP (or other) server, rather than having separate worker processes and dishing it out over the network. While there are definite advantages to the network-worker model, it makes a lot of things harder and bugs more subtle, and generally isn't the right match for a project where we don't have a lot of resources to do bulletproof network testing.
The reason I avoided this in Channels 1 was wanting to keep async and sync code separate, but after finally getting into the depths of asyncio, Twisted and threading, I have a decent solution for calling async functions from sync code and sync functions from async code, allowing the boundary to not only be crossed in-process but work a lot better than having it over the network.
This also means that the ASGI "specification" has received a major overhaul, and is now much simpler - a much worthier spiritual successor to WSGI. Much like WSGI, you provide the protocol server (e.g. a HTTP server) with an application object, which gets instantiated per-connection. You can see the new, short spec over at channels.readthedocs.io/en/2.0/asgi.html, but the key thing is that there's an instance of the application per connection (with what a "connection" is being defined per protocol - it's a request for HTTP, and the client socket lifetime for WebSocket), and an async-only main method that can await for events happening on that connection.
Thus, all protocol servers are expected to be natively async, and applications have to expose that as an interface as well. Of course, Django is synchronous, so how does that interact?
The work I mentioned before about calling sync functions from async code comes into play. Channels 2 will happily let you write fully async-native code if you like - and don't want to call the Django ORM - but it also provides plenty of helpers to write synchronous code and have it run in a threadpool alongside the server's async loop.
This means that the consumers you write in Channels 2 look just like Channels 1 - classes with synchronous code in methods: