Ticket #17258: 17258.thread-local-connections.6.diff

File 17258.thread-local-connections.6.diff, 14.7 KB (added by Julien Phalip, 13 years ago)
  • django/db/__init__.py

    diff --git a/django/db/__init__.py b/django/db/__init__.py
    index 8395468..26c7add 100644
    a b router = ConnectionRouter(settings.DATABASE_ROUTERS)  
    2222# we manually create the dictionary from the settings, passing only the
    2323# settings that the database backends care about. Note that TIME_ZONE is used
    2424# by the PostgreSQL backends.
    25 # we load all these up for backwards compatibility, you should use
     25# We load all these up for backwards compatibility, you should use
    2626# connections['default'] instead.
    27 connection = connections[DEFAULT_DB_ALIAS]
     27class DefaultConnectionProxy(object):
     28    """
     29    Proxy for accessing the default DatabaseWrapper object's attributes. If you
     30    need to access the DatabaseWrapper object itself, use
     31    connections[DEFAULT_DB_ALIAS] instead.
     32    """
     33    def __getattr__(self, item):
     34        return getattr(connections[DEFAULT_DB_ALIAS], item)
     35
     36    def __setattr__(self, name, value):
     37        return setattr(connections[DEFAULT_DB_ALIAS], name, value)
     38
     39connection = DefaultConnectionProxy()
    2840backend = load_backend(connection.settings_dict['ENGINE'])
    2941
    3042# Register an event that closes the database connection
  • django/db/backends/__init__.py

    diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
    index f2bde84..1060ec8 100644
    a b  
     1from django.db.utils import DatabaseError
     2
    13try:
    24    import thread
    35except ImportError:
    46    import dummy_thread as thread
    5 from threading import local
    67from contextlib import contextmanager
    78
    89from django.conf import settings
    from django.utils.importlib import import_module  
    1314from django.utils.timezone import is_aware
    1415
    1516
    16 class BaseDatabaseWrapper(local):
     17class BaseDatabaseWrapper(object):
    1718    """
    1819    Represents a database connection.
    1920    """
    2021    ops = None
    2122    vendor = 'unknown'
    2223
    23     def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS):
     24    def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS,
     25                 allow_thread_sharing=False):
    2426        # `settings_dict` should be a dictionary containing keys such as
    2527        # NAME, USER, etc. It's called `settings_dict` instead of `settings`
    2628        # to disambiguate it from Django settings modules.
    class BaseDatabaseWrapper(local):  
    3436        self.transaction_state = []
    3537        self.savepoint_state = 0
    3638        self._dirty = None
     39        self._thread_ident = thread.get_ident()
     40        self.allow_thread_sharing = allow_thread_sharing
    3741
    3842    def __eq__(self, other):
    3943        return self.alias == other.alias
    class BaseDatabaseWrapper(local):  
    116120                "pending COMMIT/ROLLBACK")
    117121        self._dirty = False
    118122
     123    def validate_thread_sharing(self):
     124        if (not self.allow_thread_sharing
     125            and self._thread_ident != thread.get_ident()):
     126                raise DatabaseError ("DatabaseWrapper objects created in a "
     127                    "thread can only be used in that same thread. The object"
     128                    "with alias '%s' was created in thread id %s and this is "
     129                    "thread id %s."
     130                    % (self.alias, self._thread_ident, thread.get_ident()))
     131
    119132    def is_dirty(self):
    120133        """
    121134        Returns True if the current transaction requires a commit for changes to
    class BaseDatabaseWrapper(local):  
    179192        """
    180193        Commits changes if the system is not in managed transaction mode.
    181194        """
     195        self.validate_thread_sharing()
    182196        if not self.is_managed():
    183197            self._commit()
    184198            self.clean_savepoints()
    class BaseDatabaseWrapper(local):  
    189203        """
    190204        Rolls back changes if the system is not in managed transaction mode.
    191205        """
     206        self.validate_thread_sharing()
    192207        if not self.is_managed():
    193208            self._rollback()
    194209        else:
    class BaseDatabaseWrapper(local):  
    198213        """
    199214        Does the commit itself and resets the dirty flag.
    200215        """
     216        self.validate_thread_sharing()
    201217        self._commit()
    202218        self.set_clean()
    203219
    class BaseDatabaseWrapper(local):  
    205221        """
    206222        This function does the rollback itself and resets the dirty flag.
    207223        """
     224        self.validate_thread_sharing()
    208225        self._rollback()
    209226        self.set_clean()
    210227
    class BaseDatabaseWrapper(local):  
    228245        Rolls back the most recent savepoint (if one exists). Does nothing if
    229246        savepoints are not supported.
    230247        """
     248        self.validate_thread_sharing()
    231249        if self.savepoint_state:
    232250            self._savepoint_rollback(sid)
    233251
    class BaseDatabaseWrapper(local):  
    236254        Commits the most recent savepoint (if one exists). Does nothing if
    237255        savepoints are not supported.
    238256        """
     257        self.validate_thread_sharing()
    239258        if self.savepoint_state:
    240259            self._savepoint_commit(sid)
    241260
    class BaseDatabaseWrapper(local):  
    269288        pass
    270289
    271290    def close(self):
     291        self.validate_thread_sharing()
    272292        if self.connection is not None:
    273293            self.connection.close()
    274294            self.connection = None
    275295
    276296    def cursor(self):
     297        self.validate_thread_sharing()
    277298        if (self.use_debug_cursor or
    278299            (self.use_debug_cursor is None and settings.DEBUG)):
    279300            cursor = self.make_debug_cursor(self._cursor())
  • django/db/backends/sqlite3/base.py

    diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
    index a610606..75e8fa0 100644
    a b standard library.  
    77
    88import datetime
    99import decimal
     10import warnings
    1011import re
    1112import sys
    1213
    13 from django.conf import settings
    1414from django.db import utils
    1515from django.db.backends import *
    1616from django.db.backends.signals import connection_created
    class DatabaseWrapper(BaseDatabaseWrapper):  
    241241                'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
    242242            }
    243243            kwargs.update(settings_dict['OPTIONS'])
     244            # Always allow the underlying SQLite connection to be shareable
     245            # between multiple threads. The safe-guarding will be handled at a
     246            # higher level by the `BaseDatabaseWrapper.allow_thread_sharing`
     247            # property. This is necessary as the shareability is disabled by
     248            # default in pysqlite and it cannot be changed once a connection is
     249            # opened.
     250            if 'check_same_thread' in kwargs and kwargs['check_same_thread']:
     251                warnings.warn(
     252                    'The `check_same_thread` option was provided and set to '
     253                    'True. It will be overriden with False. Use the '
     254                    '`DatabaseWrapper.allow_thread_sharing` property instead '
     255                    'for controlling thread shareability.',
     256                    RuntimeWarning
     257                )
     258            kwargs.update({'check_same_thread': False})
    244259            self.connection = Database.connect(**kwargs)
    245260            # Register extract, date_trunc, and regexp functions.
    246261            self.connection.create_function("django_extract", 2, _sqlite_extract)
  • django/db/utils.py

    diff --git a/django/db/utils.py b/django/db/utils.py
    index f0c13e3..41ad6df 100644
    a b  
    11import os
     2from threading import local
    23
    34from django.conf import settings
    45from django.core.exceptions import ImproperlyConfigured
    class ConnectionDoesNotExist(Exception):  
    5051class ConnectionHandler(object):
    5152    def __init__(self, databases):
    5253        self.databases = databases
    53         self._connections = {}
     54        self._connections = local()
    5455
    5556    def ensure_defaults(self, alias):
    5657        """
    class ConnectionHandler(object):  
    7374            conn.setdefault(setting, None)
    7475
    7576    def __getitem__(self, alias):
    76         if alias in self._connections:
    77             return self._connections[alias]
     77        if hasattr(self._connections, alias):
     78            return getattr(self._connections, alias)
    7879
    7980        self.ensure_defaults(alias)
    8081        db = self.databases[alias]
    8182        backend = load_backend(db['ENGINE'])
    8283        conn = backend.DatabaseWrapper(db, alias)
    83         self._connections[alias] = conn
     84        setattr(self._connections, alias, conn)
    8485        return conn
    8586
     87    def __setitem__(self, key, value):
     88        setattr(self._connections, key, value)
     89
    8690    def __iter__(self):
    8791        return iter(self.databases)
    8892
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index 491556b..f614dee 100644
    a b datetimes are now stored without time zone information in SQLite. When  
    673673:setting:`USE_TZ` is ``False``, if you attempt to save an aware datetime
    674674object, Django raises an exception.
    675675
     676Database connection's thread-locality
     677~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     678
     679``DatabaseWrapper`` objects (i.e. the connection objects referenced by
     680``django.db.connection`` and ``django.db.connections["some_alias"]``) used to
     681be thread-local. They are now global objects in order to be potentially shared
     682between multiple threads. While the individual connection objects are now
     683global, the ``django.db.connections`` dictionary referencing those objects is
     684still thread-local. Therefore if you just use the ORM or
     685``DatabaseWrapper.cursor()`` then the behavior is still the same as before.
     686Note, however, that ``django.db.connection`` does not directly reference the
     687default ``DatabaseWrapper`` object any more and is now a proxy to access that
     688object's attributes. If you need to access the actual ``DatabaseWrapper``
     689object, use ``django.db.connections[DEFAULT_DB_ALIAS]`` instead.
     690
     691As part of this change, all underlying SQLite connections are now enabled for
     692potential thread-sharing (by passing the ``check_same_thread=False`` attribute
     693to pysqlite). ``DatabaseWrapper`` however preserves the previous behavior by
     694disabling thread-sharing by default, so this does not affect any existing
     695code that purely relies on the ORM or on ``DatabaseWrapper.cursor()``.
     696
     697Finally, while it is now possible to pass connections between threads, Django
     698does not make any effort to synchronize access to the underlying backend.
     699Concurrency behavior is defined by the underlying backend implementation.
     700Check their documentation for details.
     701
    676702`COMMENTS_BANNED_USERS_GROUP` setting
    677703~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    678704
  • tests/regressiontests/backends/tests.py

    diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py
    index 936f010..82c21c8 100644
    a b  
    33from __future__ import with_statement, absolute_import
    44
    55import datetime
     6import threading
    67
    78from django.conf import settings
    89from django.core.management.color import no_style
    class ConnectionCreatedSignalTest(TestCase):  
    283284        connection_created.connect(receiver)
    284285        connection.close()
    285286        cursor = connection.cursor()
    286         self.assertTrue(data["connection"] is connection)
     287        self.assertTrue(data["connection"].connection is connection.connection)
    287288
    288289        connection_created.disconnect(receiver)
    289290        data.clear()
    class FkConstraintsTests(TransactionTestCase):  
    446447                        connection.check_constraints()
    447448            finally:
    448449                transaction.rollback()
     450
     451
     452class ThreadTests(TestCase):
     453
     454    def test_default_connection_thread_local(self):
     455        """
     456        Ensure that the default connection (i.e. django.db.connection) is
     457        different for each thread.
     458        Refs #17258.
     459        """
     460        connections_set = set()
     461        connection.cursor()
     462        connections_set.add(connection.connection)
     463        def runner():
     464            from django.db import connection
     465            connection.cursor()
     466            connections_set.add(connection.connection)
     467        for x in xrange(2):
     468            t = threading.Thread(target=runner)
     469            t.start()
     470            t.join()
     471        self.assertEquals(len(connections_set), 3)
     472        # Finish by closing the connections opened by the other threads (the
     473        # connection opened in the main thread will automatically be closed on
     474        # teardown).
     475        for conn in connections_set:
     476            if conn != connection.connection:
     477                conn.close()
     478
     479    def test_connections_thread_local(self):
     480        """
     481        Ensure that the connections are different for each thread.
     482        Refs #17258.
     483        """
     484        connections_set = set()
     485        for conn in connections.all():
     486            connections_set.add(conn)
     487        def runner():
     488            from django.db import connections
     489            for conn in connections.all():
     490                connections_set.add(conn)
     491        for x in xrange(2):
     492            t = threading.Thread(target=runner)
     493            t.start()
     494            t.join()
     495        self.assertEquals(len(connections_set), 6)
     496        # Finish by closing the connections opened by the other threads (the
     497        # connection opened in the main thread will automatically be closed on
     498        # teardown).
     499        for conn in connections_set:
     500            if conn != connection:
     501                conn.close()
     502
     503    def test_pass_connection_between_threads(self):
     504        """
     505        Ensure that a connection can be passed from one thread to the other.
     506        Refs #17258.
     507        """
     508        models.Person.objects.create(first_name="John", last_name="Doe")
     509
     510        def do_thread():
     511            def runner(main_thread_connection):
     512                from django.db import connections
     513                connections['default'] = main_thread_connection
     514                try:
     515                    models.Person.objects.get(first_name="John", last_name="Doe")
     516                except DatabaseError, e:
     517                    exceptions.append(e)
     518            t = threading.Thread(target=runner, args=[connections['default']])
     519            t.start()
     520            t.join()
     521
     522        # Without touching allow_thread_sharing, which should be False by default.
     523        exceptions = []
     524        do_thread()
     525        # Forbidden!
     526        self.assertTrue(isinstance(exceptions[0], DatabaseError))
     527
     528        # If explicitly setting allow_thread_sharing to False
     529        connections['default'].allow_thread_sharing = False
     530        exceptions = []
     531        do_thread()
     532        # Forbidden!
     533        self.assertTrue(isinstance(exceptions[0], DatabaseError))
     534
     535        # If explicitly setting allow_thread_sharing to True
     536        connections['default'].allow_thread_sharing = True
     537        exceptions = []
     538        do_thread()
     539        # All good
     540        self.assertEqual(len(exceptions), 0)
     541 No newline at end of file
Back to Top