summaryrefslogtreecommitdiff
path: root/django/db
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2013-02-18 11:37:26 +0100
committerAymeric Augustin <aymeric.augustin@m4x.org>2013-02-28 15:28:13 +0100
commit2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5 (patch)
tree6c1e2924741812ecae3344382fe79b757a4469a1 /django/db
parentd009ffe436410f6935798d910b0e489d53411dfa (diff)
Implemented persistent database connections.
Thanks Anssi Kääriäinen and Karen Tracey for their inputs.
Diffstat (limited to 'django/db')
-rw-r--r--django/db/__init__.py21
-rw-r--r--django/db/backends/__init__.py32
-rw-r--r--django/db/backends/mysql/base.py8
-rw-r--r--django/db/backends/oracle/base.py12
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py9
-rw-r--r--django/db/backends/sqlite3/base.py3
-rw-r--r--django/db/utils.py15
7 files changed, 90 insertions, 10 deletions
diff --git a/django/db/__init__.py b/django/db/__init__.py
index e76c6c3268..5e630392e7 100644
--- a/django/db/__init__.py
+++ b/django/db/__init__.py
@@ -42,9 +42,10 @@ class DefaultConnectionProxy(object):
connection = DefaultConnectionProxy()
backend = load_backend(connection.settings_dict['ENGINE'])
-# Register an event that closes the database connection
-# when a Django request is finished.
def close_connection(**kwargs):
+ warnings.warn(
+ "close_connection is superseded by close_old_connections.",
+ PendingDeprecationWarning, stacklevel=2)
# Avoid circular imports
from django.db import transaction
for conn in connections:
@@ -53,15 +54,25 @@ def close_connection(**kwargs):
# connection state will be cleaned up.
transaction.abort(conn)
connections[conn].close()
-signals.request_finished.connect(close_connection)
-# Register an event that resets connection.queries
-# when a Django request is started.
+# Register an event to reset saved queries when a Django request is started.
def reset_queries(**kwargs):
for conn in connections.all():
conn.queries = []
signals.request_started.connect(reset_queries)
+# Register an event to reset transaction state and close connections past
+# their lifetime. NB: abort() doesn't do anything outside of a transaction.
+def close_old_connections(**kwargs):
+ for conn in connections.all():
+ try:
+ conn.abort()
+ except DatabaseError:
+ pass
+ conn.close_if_unusable_or_obsolete()
+signals.request_started.connect(close_old_connections)
+signals.request_finished.connect(close_old_connections)
+
# Register an event that rolls back the connections
# when a Django request has an exception.
def _rollback_on_exception(**kwargs):
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 49e07cfa9e..9fb2b23644 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -1,4 +1,5 @@
import datetime
+import time
from django.db.utils import DatabaseError
@@ -49,6 +50,10 @@ class BaseDatabaseWrapper(object):
self._thread_ident = thread.get_ident()
self.allow_thread_sharing = allow_thread_sharing
+ # Connection termination related attributes
+ self.close_at = None
+ self.errors_occurred = False
+
def __eq__(self, other):
return self.alias == other.alias
@@ -59,7 +64,7 @@ class BaseDatabaseWrapper(object):
return hash(self.alias)
def wrap_database_errors(self):
- return DatabaseErrorWrapper(self.Database)
+ return DatabaseErrorWrapper(self)
def get_connection_params(self):
raise NotImplementedError
@@ -76,6 +81,11 @@ class BaseDatabaseWrapper(object):
def _cursor(self):
with self.wrap_database_errors():
if self.connection is None:
+ # Reset parameters defining when to close the connection
+ max_age = self.settings_dict['CONN_MAX_AGE']
+ self.close_at = None if max_age is None else time.time() + max_age
+ self.errors_occurred = False
+ # Establish the connection
conn_params = self.get_connection_params()
self.connection = self.get_new_connection(conn_params)
self.init_connection_state()
@@ -351,6 +361,26 @@ class BaseDatabaseWrapper(object):
self.connection = None
self.set_clean()
+ def close_if_unusable_or_obsolete(self):
+ if self.connection is not None:
+ if self.errors_occurred:
+ if self.is_usable():
+ self.errors_occurred = False
+ else:
+ self.close()
+ return
+ if self.close_at is not None and time.time() >= self.close_at:
+ self.close()
+ return
+
+ def is_usable(self):
+ """
+ Test if the database connection is usable.
+
+ This function may assume that self.connection is not None.
+ """
+ raise NotImplementedError
+
def cursor(self):
self.validate_thread_sharing()
if (self.use_debug_cursor or
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index 4bfd3c4481..6b2ecaead1 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -439,6 +439,14 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor = self.connection.cursor()
return CursorWrapper(cursor)
+ def is_usable(self):
+ try:
+ self.connection.ping()
+ except DatabaseError:
+ return False
+ else:
+ return True
+
def _rollback(self):
try:
BaseDatabaseWrapper._rollback(self)
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index d35e814d1f..478124f5df 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -598,6 +598,18 @@ class DatabaseWrapper(BaseDatabaseWrapper):
# stmtcachesize is available only in 4.3.2 and up.
pass
+ def is_usable(self):
+ try:
+ if hasattr(self.connection, 'ping'): # Oracle 10g R2 and higher
+ self.connection.ping()
+ else:
+ # Use a cx_Oracle cursor directly, bypassing Django's utilities.
+ self.connection.cursor().execute("SELECT 1 FROM DUAL")
+ except DatabaseError:
+ return False
+ else:
+ return True
+
# Oracle doesn't support savepoint commits. Ignore them.
def _savepoint_commit(self, sid):
pass
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index fb04072494..db4b5ade05 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -177,6 +177,15 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
return cursor
+ def is_usable(self):
+ try:
+ # Use a psycopg cursor directly, bypassing Django's utilities.
+ self.connection.cursor().execute("SELECT 1")
+ except DatabaseError:
+ return False
+ else:
+ return True
+
def _enter_transaction_management(self, managed):
"""
Switch the isolation level when needing transaction support, so that
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 6bf1ffc469..ad54af46ad 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -347,6 +347,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def create_cursor(self):
return self.connection.cursor(factory=SQLiteCursorWrapper)
+ def is_usable(self):
+ return True
+
def check_constraints(self, table_names=None):
"""
Checks each table name in `table_names` for rows with invalid foreign key references. This method is
diff --git a/django/db/utils.py b/django/db/utils.py
index 0c98cc23fd..cc17b3e7a3 100644
--- a/django/db/utils.py
+++ b/django/db/utils.py
@@ -56,11 +56,13 @@ class DatabaseErrorWrapper(object):
exceptions using Django's common wrappers.
"""
- def __init__(self, database):
+ def __init__(self, wrapper):
"""
- database is a module defining PEP-249 exceptions.
+ wrapper is a database wrapper.
+
+ It must have a Database attribute defining PEP-249 exceptions.
"""
- self.database = database
+ self.wrapper = wrapper
def __enter__(self):
pass
@@ -79,7 +81,7 @@ class DatabaseErrorWrapper(object):
InterfaceError,
Error,
):
- db_exc_type = getattr(self.database, dj_exc_type.__name__)
+ db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__)
if issubclass(exc_type, db_exc_type):
# Under Python 2.6, exc_value can still be a string.
try:
@@ -89,6 +91,10 @@ class DatabaseErrorWrapper(object):
dj_exc_value = dj_exc_type(*args)
if six.PY3:
dj_exc_value.__cause__ = exc_value
+ # Only set the 'errors_occurred' flag for errors that may make
+ # the connection unusable.
+ if dj_exc_type not in (DataError, IntegrityError):
+ self.wrapper.errors_occurred = True
six.reraise(dj_exc_type, dj_exc_value, traceback)
def __call__(self, func):
@@ -155,6 +161,7 @@ class ConnectionHandler(object):
conn.setdefault('ENGINE', 'django.db.backends.dummy')
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
conn['ENGINE'] = 'django.db.backends.dummy'
+ conn.setdefault('CONN_MAX_AGE', 600)
conn.setdefault('OPTIONS', {})
conn.setdefault('TIME_ZONE', 'UTC' if settings.USE_TZ else settings.TIME_ZONE)
for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']: