1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
from django.conf import settings
5
from django.db import connection
6
from django.utils.translation import ugettext as _
8
from identityprovider.readonly import readonly_manager
9
from identityprovider.views.errors import ErrorPage
12
class NoBackupError(Exception):
16
class DBFailoverMiddleware(object):
17
"""Database failover middleware.
19
This middleware will attempt to establish a minimal connection to the
20
configured database before starting each request. If it fails for
21
some reason, it will enter readonly mode and switch to some other
22
configured backup database.
24
Available database connections are configured with the DB_CONNECTIONS
25
setting, which is a list of dictionaries, each with DATABASE_ID,
26
DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER and
27
DATABASE_PASSWORD keys.
29
When the main database connection fails, the backup connections
33
def process_request(self, request):
37
max_attempts = getattr(settings, 'DBFAILOVER_ATTEMPTS', 1)
38
readonly_manager.check_readonly()
39
self.choose_connection()
40
while not connection_ok:
42
cursor = connection.cursor()
43
cursor.execute('SELECT 42')
46
# I'm just catching all exceptions here because the
47
# DB-API doesn't define a standard set of exceptions
48
# to catch, so we'd need to start catching
49
# engine-specific stuff.
51
if failed_attempts == max_attempts:
52
readonly_manager.mark_current_failed()
53
self.choose_connection()
56
return ErrorPage(500)(request)
58
def process_response(self, request, response):
59
""" Add a http header to indicate if we're currently in RO-mode """
61
response['X-Read-Only'] = str(readonly_manager.is_readonly())
64
def choose_connection(self):
65
""" Configure the first database connection that isn't already marked
68
for db in readonly_manager.connections:
69
if not readonly_manager.is_failed(db['DATABASE_ID']):
70
readonly_manager.set_db(db)
73
raise NoBackupError(_("No backup connections left"))