Supporting class-based views in async Django

Let’s see how this Discourse thing works: This topic presents the current state of discussion on adding async support to Django’s class-based views. It’s a mix of the discussions happening at the PyCon Australia 2019 sprints, and the developments afterwards.

The goal

Our goals, in line with DEP9, are to absolutely avoid breaking any current code, and also offering users the possibility (but not the default) of writing async class-based views. Async class-based views involve being able to write an async def post(self, request) or an async def get(self, request), or an async def dispatch(self, request) without rewriting an existing project, for instance in a ListView or a TemplateView.

The problem

We cannot change the method signature of View.dispatch to be a coroutine, since this would break existing user code the second anybody calls super().dispatch() – which is an encouraged pattern. But for views to be async, the dispatch method (which is called by the view method returned by View.as_view()) has to be async, to call an async get or post.

This means we need a way to allow the Django user to explicitly change the method signature of dispatch. We’ll need to figure out which of the solutions we came up with is the best, going by user experience and maintenance burden.

We should also keep in mind how third-party libraries are going to provide async-capable views open for inheritance. Should they implement each view twice? Can they offer the same view as async-capable and sync in some way (without breaking super() calls)?

Solutions

Separate async classes

The maybe most clean, separated solutions would be to provide a separate amount of async classes, like AsyncTemplateView and AsyncListView. The upside is that the user has to change only the base class, and if we provide a somewhat clever dispatch method, they don’t have to change much code, especially in third-party libraries.

At the same time, Django provides a large amounts of class-based views, so maintaining a separate stack for async seems like a lot of duplication and a rather large maintenance burden, especially since this would include for instance LoginRequiredMixin, UserPassesTestMixin, and PermissionRequiredMixin.

We could, of course, choose to provide only an AsyncView and let people handle our mixins like TemplateResponseMixin themselves, but that leaves a lot of duplicated work on the users’ side, where lots of people build AsyncFormView.

Mixins

As an interface it might instead be neat to provide an AsyncMixin, that would make classes async capable. This mixin would need to be placed leftmost (or as left as possible, maybe) in the MRO, would play pretty horribly with subclassed views or third-party view classes. Such a mixin could make sure either all of the other parent classes are async capable, or wrap the dispatch call in sync_to_async.

I’m not sure this would actually work in any meaningful way, though, especially while respecting/retaining the functionality of the other parent classes.

init_subclass

Another proposal is to adapt the current view class on the fly (though on Django startup, not on every request) via the __init_subclass__ method. This method would check for a flag on the child class, like async_enabled = True (or a decorator, or a constant, or …). If this flag is present, it would verify that all parent classes are async capable, and then replace the def dispatch method with an async def dispatch method.

This implementation is already available as a demo WIP PR here. It has the advantage of requiring neither a lot of code duplication on the Django side, nor a lot of work on the user side, and allowing third-party packages to signal compatibility fairly easily by supplying this flag. (Although the question remains how a third-party view would be able to supply a dispatch method).

We do this by monkeypatching our own code though, and I’m not sure if this is something we want to see in Django core.

Next steps

The next steps are:

  • Build working prototypes of the suggested solutions, to have a better basis for discussion.
  • Optionally: Use these prototypes in a real-life project to compare the impact.
  • Come up with alternatives
  • Discuss & decide

We should keep in mind that we can also decide against supporting class-based views. While this would be not great, a solution that places a significant burden on users or library developers may be more harmful than refering users interested in async views to use function based views for now. (And/or see what others come up with and introduce a solution later on.)

2 Likes

I seem to remember that at PyCon AU I was somewhat torn and thought __init_subclass__ was the least bad option, but as mentioned, that monkeypatching… ugh.

It’s also worth noting that whatever we do here is going to have an impact on how we structure other, similar things in the rest of Django that allow user-supplied subclasses. Middleware excluded, as that’s a separate mess.

I’m still not totally against the separate-subclasses approach here either, though it does then mean we’re baking the “async is for special pages only” approach in, rather than allowing it to be sprinkled in among existing code.

Great summary and outline, @rixx! :heart:

I’m not sure that having separate subclasses is going to help here either. You can’t have the GET and POST method on the same URL as we don’t support method-specific routing. If we were to add that before and could thus rely on its presence, the approach of sync+async for different methods could work.

1 Like

Thanks for the thread @rixx

The three mentioned mixins rely on request.user which implicitly queries the database on first access. Not sure they can run async without a rewrite - though I guess this ties into how AuthenticationMiddleware will be made async.

When doing the monkey patching, wouldn’t it need to allow all the methods in a class async, e.g. TemplateMixin.get_context_data, FormView.get_form, etc. in case they query the DB? At which point monkey-patching on __init_subclass__ looks a more like having a separate class, since it would have two implementations of everything.

I haven’t heard of any plans to do this before. I just searched old tickets and found #20479, which seemed to end up with the conclusion it’s not really necessary (?).

I guess in a case like “async for GET, sync for POST”, one could wrap POST in @sync_to_async ? But I’m not sure how common that pattern would be. Most views do similar work on different HTTP methods so would probably want to use the same calling pattern.

I’m not sure I get what you are saying. With the right base class, we could have a sync GET and an async POST on the same URL. I imagine that would be desirable, but if y’all think it’s too much of an edge case, we can drop this in our considerations, too. The dispatch method would need to be async, of course, and wrap sync methods correspondingly. (Or am I overlooking something fundamental here?)

These two comments:

and

make me think: It might be the best to intentionally postpone this topic including the design discussion until middlewares are good and ready (or are they, and I missed it?). While I’d love to push this, getting it right the first time is important, and that means relying on preceding work.

This was meant as a reply to @andrewgodwin’s post. :slightly_smiling_face:

I have the feeling that the way how class based views work will influence the way how middleware would work and vice versa. But I’m not sure I’m up to data on the middleware part either.

Well the middleware question is honestly anyone’s guess at this point, but I don’t think it has to block the discussion here; my preferred solution for middleware, if we can pull it off, would be totally useless applied to this problem.