Skip to content

Part 3: Verification

Since the Django app works as expected after integration with the library, the next step is to verify and validate its multi-tenant capabilities.

Step 1: Adding a new tenant

Let's start by adding a new tenant.

The library exposes a bunch of HTTP endpoints which should be added to the root urlconf as follows:

# mt_site/urls.py

from django.urls import path, include
from mt_demo.views import HospitalView

...
urlpatterns = [
    path('hospital/', HospitalView.as_view()),
    path('api/', include('tenant_router.urls')),
]
...

These endpoints among other things perform CRUD operations on a tenant. Before proceeding to call one of these HTTP endpoints, add the following code:

# mt_demo/apps.py

from django.apps import AppConfig
from django.core.management import call_command
from django.conf import settings

from tenant_router.tenant_channel_observer import (
    tenant_channel_observable,
    TenantLifecycleEvent
)
from tenant_router.bootstrap import on_worker_init


class MtDemoConfig(AppConfig):
    name = 'mt_demo'

    def _post_tenant_create(self, event):
        tenant_id = event.data["tenant_id"]
        call_command('migrate_all', tenant_id=tenant_id)

    def ready(self):
        if settings.DEBUG:
            on_worker_init()

        tenant_channel_observable.subscribe(
            lifecycle_event=TenantLifecycleEvent.POST_TENANT_CREATE,
            callback=self._post_tenant_create
        )

Though the above code snippet seems fairly large and complex, to put in a nutshell, the following is what it achieves:

  1. on_worker_init: Starts a background event listener (thread) which listens to events on a few channels.
  2. tenant_channel_observable.subscribe(): Taps into an observable stream to invoke a callback when a particular event is fired, in this case, POST_TENANT_CREATE.
  3. _post_tenant_create: A callback which is invoked when the POST_TENANT_CREATE event is fired. Runs the migrate_all management command which performs migration operations across one or more databases for the newly added tenant.

Note

To get a better understanding of what this does and how it works internally, check out the reactive configuration page.

The following section will give a better intuition of how all of this fits together. To start off, let's start the server and call the Add tenant HTTP endpoint.

$ python manage.py runserver

Once the server is started, make a HTTP POST request to the endpoint /api/tenant/ as follows:

$ python manage.py shell
>>> import requests
>>> payload = {
...     "tenant_id": "tenant-2.test.com",
...     "deploy_info": {
...         "POSTGRES_URL": "postgres://{your_db_username}:{your_db_password}@127.0.0.1:5432/tenant_2",
...     }
... }
>>> r = requests.post(
...     "http://localhost:8000/api/tenant/",
...     json=payload
... )
>>> print(r.json())

The expected response is as follows:

{
    "tenant_id": "tenant-2.test.com",
    "lifecycle_event": "tenant_create"
}

This confirms that a new tenant with id tenant-2.test.com has been created. Its respective metadata has been loaded into the config store and subsequently the tenant_create lifecycle event has been fired.

Step 2: Verifying

Now to verify whether the tenant has actually been added, let's make a request to the same HTTP endpoint as above, but this time, the value for the x-tenant-id header will be
tenant-2.test.com

$ python manage.py shell
>>> import requests
>>> r = requests.get(
...     'http://127.0.0.1:8000/hospital/', 
...     headers={"x-tenant-id": "tenant-2.test.com"}
... )
>>> print(r.json())

The expected response is as follows:

{
    "data": [],
    "tenant_id": "tenant-2.test.com"
}

Info

It might be a bit odd that without running migrations explictly for the newly created tenant_2 database, the query succeeded. The answer lies in the _post_tenant_create callback. It gets scheduled to run when the tenant_create event is fired and is invoked subsequently when the next HTTP request hits the server. This in-turn runs migrations before the view is called.

As a follow up, the HTTP request made above can be repeated with the value of x-tenant-id set to tenant-1.test.com. The expected response is the same as the one mentioned in part 2 of this tutorial.

Note

Another simple follow up exercise would be to verify if Redis contains the metadata of the newly added tenant.

Voila! That marks the end of this tutorial. The following points summarise what we've achieved as a result of integrating the library.

  1. Ability to dynamically route queries to the appropriate tenant databases based on the tenant context in the HTTP request.
  2. Ability to add/update/delete tenant configuration on the fly without a graceful restart of the server.

Where to next ?

The following are a list of items that you might want to explore first, in order to get yourself familiarised with the processes to be followed, in the new multi-tenant paradigm.

  • Playground: Describes how to use the playground app to explore and verify various facets of the library.

  • DB Migrations: Describes the strategies and methods involved in performing database migrations across multiple tenants.

  • Caches: Describes in detail about how to use the Caching framework provided by Django in the new multi-tenant paradigm.

  • Unit tests: Describes the methodologies to be followed for writing unit /functional tests in the new multi-tenant paradigm.

  • Settings: Describes the various settings that can be configured from the settings.py module to control the behaviour of various functionalities of the library.

  • Configuration file: Describes, in detail, the schema for the tenant_config.json file and the rationale behind it.