Skip to content

Commit

Permalink
Add support for read-only database replica
Browse files Browse the repository at this point in the history
  • Loading branch information
davea committed Nov 24, 2023
1 parent 4536e48 commit 617375f
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 0 deletions.
22 changes: 22 additions & 0 deletions labour_project/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from .multidb import use_primary

READ_METHODS = frozenset(['GET', 'HEAD', 'OPTIONS', 'TRACE'])


def force_primary_middleware(get_response):
"""On a non-read request (e.g. POST), always use the primary database, and
set a short-lived cookie. Then on any request made including that cookie,
also always use the primary database. This is so we don't need to worry
about any replication lag for reads made immediately after writes, because
we force those reads to go to the primary."""
def middleware(request):
use_primary('primary_forced' in request.COOKIES or request.method not in READ_METHODS)

response = get_response(request)

if request.method not in READ_METHODS:
response.set_cookie('primary_forced', value='y', max_age=10)

return response

return middleware
16 changes: 16 additions & 0 deletions labour_project/multidb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
A getter/setter for a thread-local variable for using the primary database.
"""

import threading

_locals = threading.local()


def use_primary(val=None):
"""If passed no argument, return the current value (or False if unset);
if passed an argument, set the variable to that."""
if val is None:
return getattr(_locals, 'primary', False)
else:
_locals.primary = val
45 changes: 45 additions & 0 deletions labour_project/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import sys
import yaml
from .utils import skip_unreadable_post

Expand Down Expand Up @@ -97,6 +98,47 @@
}
}

if config.get('MAPIT_DB_RO_HOST', '') and 'test' not in sys.argv:
DATABASES['replica'] = {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': config.get('MAPIT_DB_NAME', 'mapit'),
'USER': config.get('MAPIT_DB_USER', 'mapit'),
'PASSWORD': config.get('MAPIT_DB_PASS', ''),
'HOST': config['MAPIT_DB_RO_HOST'],
'PORT': config['MAPIT_DB_RO_PORT'],
# Should work, but does not appear to (hence sys.argv test above); see
# https://stackoverflow.com/questions/33941139/test-mirror-default-database-but-no-data
# 'TEST': {
# 'MIRROR': 'default',
# },
}

import random
from .multidb import use_primary

class PrimaryReplicaRouter(object):
"""A basic primary/replica database router."""
def db_for_read(self, model, **hints):
"""Randomly pick between default and replica databases, unless the
request (via middleware) demands we use the primary."""
if use_primary():
return 'default'
return random.choice(['default', 'replica'])

def db_for_write(self, model, **hints):
"""Always write to the primary database."""
return 'default'

def allow_relation(self, obj1, obj2, **hints):
"""Any relation between objects is allowed, as same data."""
return True

def allow_migrate(self, db, app_label, model_name=None, **hints):
"""migrate is only ever called on the default database."""
return True

DATABASE_ROUTERS = ['labour_project.settings.PrimaryReplicaRouter']

# Make this unique, and don't share it with anybody.
SECRET_KEY = config.get('DJANGO_SECRET_KEY', '')

Expand Down Expand Up @@ -171,6 +213,9 @@
'mapit.middleware.ViewExceptionMiddleware',
]

if config.get('MAPIT_DB_RO_HOST', '') and 'test' not in sys.argv:
MIDDLEWARE.insert(0, 'labour_project.middleware.force_primary_middleware')

if DEBUG:
MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')

Expand Down

0 comments on commit 617375f

Please sign in to comment.