Ticket #17258: 17258.thread-local-connections.6.diff
File 17258.thread-local-connections.6.diff, 14.7 KB (added by , 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) 22 22 # we manually create the dictionary from the settings, passing only the 23 23 # settings that the database backends care about. Note that TIME_ZONE is used 24 24 # by the PostgreSQL backends. 25 # we load all these up for backwards compatibility, you should use25 # We load all these up for backwards compatibility, you should use 26 26 # connections['default'] instead. 27 connection = connections[DEFAULT_DB_ALIAS] 27 class 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 39 connection = DefaultConnectionProxy() 28 40 backend = load_backend(connection.settings_dict['ENGINE']) 29 41 30 42 # 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 1 from django.db.utils import DatabaseError 2 1 3 try: 2 4 import thread 3 5 except ImportError: 4 6 import dummy_thread as thread 5 from threading import local6 7 from contextlib import contextmanager 7 8 8 9 from django.conf import settings … … from django.utils.importlib import import_module 13 14 from django.utils.timezone import is_aware 14 15 15 16 16 class BaseDatabaseWrapper( local):17 class BaseDatabaseWrapper(object): 17 18 """ 18 19 Represents a database connection. 19 20 """ 20 21 ops = None 21 22 vendor = 'unknown' 22 23 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): 24 26 # `settings_dict` should be a dictionary containing keys such as 25 27 # NAME, USER, etc. It's called `settings_dict` instead of `settings` 26 28 # to disambiguate it from Django settings modules. … … class BaseDatabaseWrapper(local): 34 36 self.transaction_state = [] 35 37 self.savepoint_state = 0 36 38 self._dirty = None 39 self._thread_ident = thread.get_ident() 40 self.allow_thread_sharing = allow_thread_sharing 37 41 38 42 def __eq__(self, other): 39 43 return self.alias == other.alias … … class BaseDatabaseWrapper(local): 116 120 "pending COMMIT/ROLLBACK") 117 121 self._dirty = False 118 122 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 119 132 def is_dirty(self): 120 133 """ 121 134 Returns True if the current transaction requires a commit for changes to … … class BaseDatabaseWrapper(local): 179 192 """ 180 193 Commits changes if the system is not in managed transaction mode. 181 194 """ 195 self.validate_thread_sharing() 182 196 if not self.is_managed(): 183 197 self._commit() 184 198 self.clean_savepoints() … … class BaseDatabaseWrapper(local): 189 203 """ 190 204 Rolls back changes if the system is not in managed transaction mode. 191 205 """ 206 self.validate_thread_sharing() 192 207 if not self.is_managed(): 193 208 self._rollback() 194 209 else: … … class BaseDatabaseWrapper(local): 198 213 """ 199 214 Does the commit itself and resets the dirty flag. 200 215 """ 216 self.validate_thread_sharing() 201 217 self._commit() 202 218 self.set_clean() 203 219 … … class BaseDatabaseWrapper(local): 205 221 """ 206 222 This function does the rollback itself and resets the dirty flag. 207 223 """ 224 self.validate_thread_sharing() 208 225 self._rollback() 209 226 self.set_clean() 210 227 … … class BaseDatabaseWrapper(local): 228 245 Rolls back the most recent savepoint (if one exists). Does nothing if 229 246 savepoints are not supported. 230 247 """ 248 self.validate_thread_sharing() 231 249 if self.savepoint_state: 232 250 self._savepoint_rollback(sid) 233 251 … … class BaseDatabaseWrapper(local): 236 254 Commits the most recent savepoint (if one exists). Does nothing if 237 255 savepoints are not supported. 238 256 """ 257 self.validate_thread_sharing() 239 258 if self.savepoint_state: 240 259 self._savepoint_commit(sid) 241 260 … … class BaseDatabaseWrapper(local): 269 288 pass 270 289 271 290 def close(self): 291 self.validate_thread_sharing() 272 292 if self.connection is not None: 273 293 self.connection.close() 274 294 self.connection = None 275 295 276 296 def cursor(self): 297 self.validate_thread_sharing() 277 298 if (self.use_debug_cursor or 278 299 (self.use_debug_cursor is None and settings.DEBUG)): 279 300 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. 7 7 8 8 import datetime 9 9 import decimal 10 import warnings 10 11 import re 11 12 import sys 12 13 13 from django.conf import settings14 14 from django.db import utils 15 15 from django.db.backends import * 16 16 from django.db.backends.signals import connection_created … … class DatabaseWrapper(BaseDatabaseWrapper): 241 241 'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES, 242 242 } 243 243 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}) 244 259 self.connection = Database.connect(**kwargs) 245 260 # Register extract, date_trunc, and regexp functions. 246 261 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 1 1 import os 2 from threading import local 2 3 3 4 from django.conf import settings 4 5 from django.core.exceptions import ImproperlyConfigured … … class ConnectionDoesNotExist(Exception): 50 51 class ConnectionHandler(object): 51 52 def __init__(self, databases): 52 53 self.databases = databases 53 self._connections = {}54 self._connections = local() 54 55 55 56 def ensure_defaults(self, alias): 56 57 """ … … class ConnectionHandler(object): 73 74 conn.setdefault(setting, None) 74 75 75 76 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) 78 79 79 80 self.ensure_defaults(alias) 80 81 db = self.databases[alias] 81 82 backend = load_backend(db['ENGINE']) 82 83 conn = backend.DatabaseWrapper(db, alias) 83 se lf._connections[alias] = conn84 setattr(self._connections, alias, conn) 84 85 return conn 85 86 87 def __setitem__(self, key, value): 88 setattr(self._connections, key, value) 89 86 90 def __iter__(self): 87 91 return iter(self.databases) 88 92 -
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 673 673 :setting:`USE_TZ` is ``False``, if you attempt to save an aware datetime 674 674 object, Django raises an exception. 675 675 676 Database 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 681 be thread-local. They are now global objects in order to be potentially shared 682 between multiple threads. While the individual connection objects are now 683 global, the ``django.db.connections`` dictionary referencing those objects is 684 still thread-local. Therefore if you just use the ORM or 685 ``DatabaseWrapper.cursor()`` then the behavior is still the same as before. 686 Note, however, that ``django.db.connection`` does not directly reference the 687 default ``DatabaseWrapper`` object any more and is now a proxy to access that 688 object's attributes. If you need to access the actual ``DatabaseWrapper`` 689 object, use ``django.db.connections[DEFAULT_DB_ALIAS]`` instead. 690 691 As part of this change, all underlying SQLite connections are now enabled for 692 potential thread-sharing (by passing the ``check_same_thread=False`` attribute 693 to pysqlite). ``DatabaseWrapper`` however preserves the previous behavior by 694 disabling thread-sharing by default, so this does not affect any existing 695 code that purely relies on the ORM or on ``DatabaseWrapper.cursor()``. 696 697 Finally, while it is now possible to pass connections between threads, Django 698 does not make any effort to synchronize access to the underlying backend. 699 Concurrency behavior is defined by the underlying backend implementation. 700 Check their documentation for details. 701 676 702 `COMMENTS_BANNED_USERS_GROUP` setting 677 703 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 678 704 -
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 3 3 from __future__ import with_statement, absolute_import 4 4 5 5 import datetime 6 import threading 6 7 7 8 from django.conf import settings 8 9 from django.core.management.color import no_style … … class ConnectionCreatedSignalTest(TestCase): 283 284 connection_created.connect(receiver) 284 285 connection.close() 285 286 cursor = connection.cursor() 286 self.assertTrue(data["connection"] isconnection)287 self.assertTrue(data["connection"].connection is connection.connection) 287 288 288 289 connection_created.disconnect(receiver) 289 290 data.clear() … … class FkConstraintsTests(TransactionTestCase): 446 447 connection.check_constraints() 447 448 finally: 448 449 transaction.rollback() 450 451 452 class 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