Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Python 3.7 contextvars to pass state between Django views

I'm building a single database/shared schema multi-tenant application using Django 2.2 and Python 3.7.

I'm attempting to use the new contextvars api to share the tenant state (an Organization) between views.

I'm setting the state in a custom middleware like this:

# tenant_middleware.py

from organization.models import Organization
import contextvars
import tenant.models as tenant_model


tenant = contextvars.ContextVar('tenant', default=None)

class TenantMiddleware:
   def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        user = request.user

        if user.is_authenticated:
            organization = Organization.objects.get(organizationuser__is_current_organization=True, organizationuser__user=user)
            tenant_object = tenant_model.Tenant.objects.get(organization=organization)
            tenant.set(tenant_object)

        return response

I'm using this state by having my app's models inherit from a TenantAwareModel like this:

# tenant_models.py

from django.contrib.auth import get_user_model
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from organization.models import Organization
from tenant_middleware import tenant

User = get_user_model()


class TenantManager(models.Manager):
    def get_queryset(self, *args, **kwargs):
        tenant_object = tenant.get()

        if tenant_object:
            return super(TenantManager, self).get_queryset(*args, **kwargs).filter(tenant=tenant_object)
        else:
            return None

    @receiver(pre_save)
    def pre_save_callback(sender, instance, **kwargs):
        tenant_object = tenant.get()
        instance.tenant = tenant_object


class Tenant(models.Model):
    organization = models.ForeignKey(Organization, null=False, on_delete=models.CASCADE)

    def __str__(self):
        return self.organization.name


class TenantAwareModel(models.Model):
    tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='%(app_label)s_%(class)s_related', related_query_name='%(app_label)s_%(class)ss')
    objects = models.Manager()
    tenant_objects = TenantManager()

    class Meta:
        abstract = True

In my application the business logic can then retrieve querysets using .tenant_objects... on a model class rather than .objects...

The problem I'm having is that it doesn't always work - specifically in these cases:

  1. In my login view after login() is called, the middleware runs and I can see the tenant is set correctly. When I redirect from my login view to my home view, however, the state is (initially) empty again and seems to get set properly after the home view executes. If I reload the home view, everything works fine.

  2. If I logout and then login again as a different user, the state from the previous user is retained, again until a do a reload of the page. This seems related to the previous issue, as it almost seems like the state is lagging (for lack of a better word).

  3. I use Celery to spin off shared_tasks for processing. I have to manually pass the tenant to these, as they don't pick up the context.

Questions:

  1. Am I doing this correctly?

  2. Do I need to manually reload the state somehow in each module?

Frustrated, as I can find almost no examples of doing this and very little discussion of contextvars. I'm trying to avoid passing the tenant around manually everywhere or using thread.locals.

Thanks.

like image 794
tunecrew Avatar asked Oct 29 '25 06:10

tunecrew


1 Answers

You're only setting the context after the response has been generated. That means it will always lag. You probably want to set it before, then check after if the user has changed.

Note though that I'm not really sure this will ever work exactly how you want. Context vars are by definition local; but in an environment like Django you can never guarantee that consecutive requests from the same user will be served by the same server process, and similarly one process can serve requests from multiple users. Plus, as you've noted, Celery is a yet another separate process again, which won't share the context.

like image 179
Daniel Roseman Avatar answered Oct 31 '25 22:10

Daniel Roseman