├── .gitignore ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.md ├── dbpool ├── __init__.py └── db │ ├── __init__.py │ └── backends │ ├── __init__.py │ ├── postgis │ ├── __init__.py │ └── base.py │ └── postgresql_psycopg2 │ ├── __init__.py │ └── base.py ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .Python 4 | .project 5 | .pydevproject 6 | .settings 7 | bin 8 | build 9 | dist 10 | include 11 | lib 12 | share 13 | src 14 | django_db_pool.egg-info 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Primary authors: 2 | 3 | * Greg McGuire 4 | 5 | Contributors: 6 | 7 | * Jan Wrobel (wrr) 8 | * David Mugnai (cinghiale) 9 | * Peter Baumgartner (ipmb) 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Greg McGuire 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Bruno Renié nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS README.md LICENSE 2 | recursive-include dbpool *.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django DB Pool 2 | ============= 3 | 4 | **Note that this code has not been rigorously tested in high-volume production systems! You should perform your own 5 | load / concurrency tests prior to any deployment. And of course, patches are highly appreciated.** 6 | 7 | Another connection pool "solution"? 8 | ----------------------------------- 9 | 10 | Yes, alas. Django punts on the problem of pooled / persistant connections ([1][1]), generally telling folks to use a 11 | dedicated application like PGBouncer (for Postgres.) However that's not always workable on app-centric platforms like 12 | Heroku, where each application runs in isolation. Thus this package. There are others ([2][2]), but this one attempts 13 | to provide connection persistance / pooling with as few dependencies as possible. 14 | 15 | Currently only the Django's postgres_psycopg2 / postgis drivers are supported. Connection pooling is implemented by 16 | thinly wrapping a psycopg2 connection object with a pool-aware class. The actual pool implementation is psycop2g's 17 | built-in [ThreadedConnectionPool](http://initd.org/psycopg/docs/pool.html), which handles thread safety for the pool 18 | instance, as well as simple dead connection testing when connections are returned. 19 | 20 | Because this implementation sits inside the python interpreter, in a multi-process app server environment the pool will 21 | never be larger than one connection. However, you can still benefit from connection persistance (no connection creation 22 | overhead, query plan caching, etc.) so the (minimal) additional overhead of the pool should be outweighed by these 23 | benefits. TODO: back this up with some data! 24 | 25 | 26 | Requirements 27 | ------------ 28 | 29 | * [Django 1.3 - 1.5](https://www.djangoproject.com/download/) 30 | * [PostgreSQL](http://www.postgresql.org/) or [PostGIS](http://postgis.net/) for your database 31 | 32 | 33 | Installation 34 | ------------ 35 | 36 | pip install django-db-pool 37 | 38 | 39 | Usage 40 | ----- 41 | 42 | * PostgreSQL 43 | * Change your `DATABASES` -> `ENGINE` from `'django.db.backends.postgresql_psycopg2'` to `'dbpool.db.backends.postgresql_psycopg2'`. 44 | * PostGIS 45 | * Change your `DATABASES` -> `ENGINE` from `'django.contrib.gis.db.backends.postgis'` to `'dbpool.db.backends.postgis'`. 46 | 47 | If you are in a multithreaded environment, also set `MAX_CONNS` and optionally `MIN_CONNS` in the `OPTIONS`, 48 | like this: 49 | 50 | 'default': { 51 | 'ENGINE': 'dbpool.db.backends.postgresql_psycopg2', 52 | 'OPTIONS': {'MAX_CONNS': 1}, 53 | # These options will be used to generate the connection pool instance 54 | # on first use and should remain unchanged from your previous entries 55 | 'NAME': 'test', 56 | 'USER': 'test', 57 | 'PASSWORD': 'test123', 58 | 'HOST': 'localhost', 59 | 'PORT': '', 60 | } 61 | 62 | See the [code][base] for more information on settings `MAX_CONNS` and `MIN_CONNS`. 63 | 64 | You can set `TEST_ON_BORROW` (also in the `OPTIONS`) to True if you would like a connection to be validated each time it is 65 | checked out. If you enable this, any connection that fails a test query will be discarded from the pool and a new connection 66 | fetched, retrying up to the largest size of the pool. Since this incurs some overhead you should weigh it against the 67 | benefit of transparently recovering from database connection failures. 68 | 69 | Lastly, if you use [South](http://south.aeracode.org/) (and you should!) you'll want to make sure it knows that you're still 70 | using Postgres: 71 | 72 | SOUTH_DATABASE_ADAPTERS = { 73 | 'default': 'south.db.postgresql_psycopg2', 74 | } 75 | 76 | [1]: https://groups.google.com/d/topic/django-users/m1jeE4Cxr9A/discussion 77 | [2]: https://github.com/jinzo/django-dbpool-backend 78 | [base]: https://github.com/gmcguire/django-db-pool/blob/0.0.8/dbpool/db/backends/postgresql_psycopg2/base.py#L47-60 79 | 80 | -------------------------------------------------------------------------------- /dbpool/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmcguire/django-db-pool/d4e0aa6a150fd7bd2024e079cd3b7147ea341e63/dbpool/__init__.py -------------------------------------------------------------------------------- /dbpool/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmcguire/django-db-pool/d4e0aa6a150fd7bd2024e079cd3b7147ea341e63/dbpool/db/__init__.py -------------------------------------------------------------------------------- /dbpool/db/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmcguire/django-db-pool/d4e0aa6a150fd7bd2024e079cd3b7147ea341e63/dbpool/db/backends/__init__.py -------------------------------------------------------------------------------- /dbpool/db/backends/postgis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmcguire/django-db-pool/d4e0aa6a150fd7bd2024e079cd3b7147ea341e63/dbpool/db/backends/postgis/__init__.py -------------------------------------------------------------------------------- /dbpool/db/backends/postgis/base.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Pooled PostGIS database backend for Django. 3 | 4 | Requires psycopg 2: http://initd.org/projects/psycopg2 5 | 6 | Created on Feb 22, 2013 7 | 8 | @author: greg 9 | ''' 10 | from dbpool.db.backends.postgresql_psycopg2.base import DatabaseWrapper as PooledDatabaseWrapper 11 | from django.contrib.gis.db.backends.postgis.creation import PostGISCreation 12 | from django.contrib.gis.db.backends.postgis.introspection import PostGISIntrospection 13 | from django.contrib.gis.db.backends.postgis.operations import PostGISOperations 14 | 15 | class DatabaseWrapper(PooledDatabaseWrapper): 16 | def __init__(self, *args, **kwargs): 17 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 18 | self.creation = PostGISCreation(self) 19 | self.ops = PostGISOperations(self) 20 | self.introspection = PostGISIntrospection(self) -------------------------------------------------------------------------------- /dbpool/db/backends/postgresql_psycopg2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmcguire/django-db-pool/d4e0aa6a150fd7bd2024e079cd3b7147ea341e63/dbpool/db/backends/postgresql_psycopg2/__init__.py -------------------------------------------------------------------------------- /dbpool/db/backends/postgresql_psycopg2/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pooled PostgreSQL database backend for Django. 3 | 4 | Requires psycopg 2: http://initd.org/projects/psycopg2 5 | """ 6 | from django import get_version as get_django_version 7 | from django.db.backends.postgresql_psycopg2.base import \ 8 | DatabaseWrapper as OriginalDatabaseWrapper 9 | from django.db.backends.signals import connection_created 10 | from threading import Lock 11 | import logging 12 | import sys 13 | 14 | try: 15 | import psycopg2 as Database 16 | import psycopg2.extensions 17 | except ImportError, e: 18 | from django.core.exceptions import ImproperlyConfigured 19 | raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e) 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | class PooledConnection(): 24 | ''' 25 | Thin wrapper around a psycopg2 connection to handle connection pooling. 26 | ''' 27 | def __init__(self, pool, test_query=None): 28 | self._pool = pool 29 | 30 | # If passed a test query we'll run it to ensure the connection is available 31 | if test_query: 32 | self._wrapped_connection = None 33 | num_attempts = 0 34 | while self._wrapped_connection is None: 35 | num_attempts += 1; 36 | c = pool.getconn() 37 | try: 38 | c.cursor().execute(test_query) 39 | except Database.Error: 40 | pool.putconn(c, close=True) 41 | if num_attempts > self._pool.maxconn: 42 | logger.error("Unable to check out connection from pool %s" % self._pool) 43 | raise; 44 | else: 45 | logger.info("Closing dead connection from pool %s" % self._pool, 46 | exc_info=sys.exc_info()) 47 | else: 48 | if not c.autocommit: 49 | c.rollback() 50 | self._wrapped_connection = c 51 | else: 52 | self._wrapped_connection = pool.getconn() 53 | 54 | logger.debug("Checked out connection %s from pool %s" % (self._wrapped_connection, self._pool)) 55 | 56 | def close(self): 57 | ''' 58 | Override to return the connection to the pool rather than closing it. 59 | ''' 60 | if self._wrapped_connection and self._pool: 61 | logger.debug("Returning connection %s to pool %s" % (self._wrapped_connection, self._pool)) 62 | self._pool.putconn(self._wrapped_connection) 63 | self._wrapped_connection = None 64 | 65 | def __getattr__(self, attr): 66 | ''' 67 | All other calls proxy through to the "real" connection 68 | ''' 69 | return getattr(self._wrapped_connection, attr) 70 | 71 | ''' 72 | This holds our connection pool instances (for each alias in settings.DATABASES that 73 | uses our PooledDatabaseWrapper.) 74 | ''' 75 | connection_pools = {} 76 | connection_pools_lock = Lock() 77 | 78 | pool_config_defaults = { 79 | 'MIN_CONNS': None, 80 | 'MAX_CONNS': 1, 81 | 'TEST_ON_BORROW': False, 82 | 'TEST_ON_BORROW_QUERY': 'SELECT 1' 83 | } 84 | 85 | def _set_up_pool_config(self): 86 | ''' 87 | Helper to configure pool options during DatabaseWrapper initialization. 88 | ''' 89 | self._max_conns = self.settings_dict['OPTIONS'].get('MAX_CONNS', pool_config_defaults['MAX_CONNS']) 90 | self._min_conns = self.settings_dict['OPTIONS'].get('MIN_CONNS', self._max_conns) 91 | 92 | self._test_on_borrow = self.settings_dict["OPTIONS"].get('TEST_ON_BORROW', 93 | pool_config_defaults['TEST_ON_BORROW']) 94 | if self._test_on_borrow: 95 | self._test_on_borrow_query = self.settings_dict["OPTIONS"].get('TEST_ON_BORROW_QUERY', 96 | pool_config_defaults['TEST_ON_BORROW_QUERY']) 97 | else: 98 | self._test_on_borrow_query = None 99 | 100 | 101 | def _create_connection_pool(self, conn_params): 102 | ''' 103 | Helper to initialize the connection pool. 104 | ''' 105 | connection_pools_lock.acquire() 106 | try: 107 | # One more read to prevent a read/write race condition (We do this 108 | # here to avoid the overhead of locking each time we get a connection.) 109 | if (self.alias not in connection_pools or 110 | connection_pools[self.alias]['settings'] != self.settings_dict): 111 | logger.info("Creating connection pool for db alias %s" % self.alias) 112 | logger.info(" using MIN_CONNS = %s, MAX_CONNS = %s, TEST_ON_BORROW = %s" % (self._min_conns, 113 | self._max_conns, 114 | self._test_on_borrow)) 115 | 116 | from psycopg2 import pool 117 | connection_pools[self.alias] = { 118 | 'pool': pool.ThreadedConnectionPool(self._min_conns, self._max_conns, **conn_params), 119 | 'settings': dict(self.settings_dict), 120 | } 121 | finally: 122 | connection_pools_lock.release() 123 | 124 | 125 | ''' 126 | Simple Postgres pooled connection that uses psycopg2's built-in ThreadedConnectionPool 127 | implementation. In Django, use this by specifying MAX_CONNS and (optionally) MIN_CONNS 128 | in the OPTIONS dictionary for the given db entry in settings.DATABASES. 129 | 130 | MAX_CONNS should be equal to the maximum number of threads your app server is configured 131 | for. For example, if you are running Gunicorn or Apache/mod_wsgi (in a multiple *process* 132 | configuration) MAX_CONNS should be set to 1, since you'll have a dedicated python 133 | interpreter per process/worker. If you're running Apache/mod_wsgi in a multiple *thread* 134 | configuration set MAX_CONNS to the number of threads you have configured for each process. 135 | 136 | By default MIN_CONNS will be set to MAX_CONNS, which prevents connections from being closed. 137 | If your load is spikey and you want to recycle connections, set MIN_CONNS to something lower 138 | than MAX_CONNS. I suggest it should be no lower than your 95th percentile concurrency for 139 | your app server. 140 | 141 | If you wish to validate connections on each check out, specify TEST_ON_BORROW (set to True) 142 | in the OPTIONS dictionary for the given db entry. You can also provide an optional 143 | TEST_ON_BORROW_QUERY, which is "SELECT 1" by default. 144 | ''' 145 | class DatabaseWrapper16(OriginalDatabaseWrapper): 146 | ''' 147 | For Django 1.6.x 148 | 149 | TODO: See https://github.com/django/django/commit/1893467784deb6cd8a493997e8bac933cc2e4af9 150 | but more importantly https://github.com/django/django/commit/2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5 151 | 152 | This code may be no longer needed! 153 | ''' 154 | set_up_pool_config = _set_up_pool_config 155 | create_connection_pool = _create_connection_pool 156 | 157 | def __init__(self, *args, **kwargs): 158 | super(DatabaseWrapper16, self).__init__(*args, **kwargs) 159 | self.set_up_pool_config() 160 | 161 | def get_new_connection(self, conn_params): 162 | # Is this the initial use of the global connection_pools dictionary for 163 | # this python interpreter? Build a ThreadedConnectionPool instance and 164 | # add it to the dictionary if so. 165 | if self.alias not in connection_pools or connection_pools[self.alias]['settings'] != self.settings_dict: 166 | for extra in pool_config_defaults.keys(): 167 | if extra in conn_params: 168 | del conn_params[extra] 169 | 170 | self.create_connection_pool(conn_params) 171 | 172 | return PooledConnection(connection_pools[self.alias]['pool'], test_query=self._test_on_borrow_query) 173 | 174 | 175 | class DatabaseWrapper14and15(OriginalDatabaseWrapper): 176 | ''' 177 | For Django 1.4.x and 1.5.x 178 | ''' 179 | set_up_pool_config = _set_up_pool_config 180 | create_connection_pool = _create_connection_pool 181 | 182 | def __init__(self, *args, **kwargs): 183 | super(DatabaseWrapper14and15, self).__init__(*args, **kwargs) 184 | self.set_up_pool_config() 185 | 186 | def _cursor(self): 187 | settings_dict = self.settings_dict 188 | if self.connection is None or connection_pools[self.alias]['settings'] != settings_dict: 189 | # Is this the initial use of the global connection_pools dictionary for 190 | # this python interpreter? Build a ThreadedConnectionPool instance and 191 | # add it to the dictionary if so. 192 | if self.alias not in connection_pools or connection_pools[self.alias]['settings'] != settings_dict: 193 | if not settings_dict['NAME']: 194 | from django.core.exceptions import ImproperlyConfigured 195 | raise ImproperlyConfigured( 196 | "settings.DATABASES is improperly configured. " 197 | "Please supply the NAME value.") 198 | conn_params = { 199 | 'database': settings_dict['NAME'], 200 | } 201 | conn_params.update(settings_dict['OPTIONS']) 202 | for extra in ['autocommit'] + pool_config_defaults.keys(): 203 | if extra in conn_params: 204 | del conn_params[extra] 205 | if settings_dict['USER']: 206 | conn_params['user'] = settings_dict['USER'] 207 | if settings_dict['PASSWORD']: 208 | conn_params['password'] = force_str(settings_dict['PASSWORD']) 209 | if settings_dict['HOST']: 210 | conn_params['host'] = settings_dict['HOST'] 211 | if settings_dict['PORT']: 212 | conn_params['port'] = settings_dict['PORT'] 213 | 214 | self.create_connection_pool(conn_params) 215 | 216 | self.connection = PooledConnection(connection_pools[self.alias]['pool'], 217 | test_query=self._test_on_borrow_query) 218 | self.connection.set_client_encoding('UTF8') 219 | tz = 'UTC' if settings.USE_TZ else settings_dict.get('TIME_ZONE') 220 | if tz: 221 | try: 222 | get_parameter_status = self.connection.get_parameter_status 223 | except AttributeError: 224 | # psycopg2 < 2.0.12 doesn't have get_parameter_status 225 | conn_tz = None 226 | else: 227 | conn_tz = get_parameter_status('TimeZone') 228 | 229 | if conn_tz != tz: 230 | # Set the time zone in autocommit mode (see #17062) 231 | self.connection.set_isolation_level( 232 | psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) 233 | self.connection.cursor().execute( 234 | self.ops.set_time_zone_sql(), [tz]) 235 | self.connection.set_isolation_level(self.isolation_level) 236 | self._get_pg_version() 237 | connection_created.send(sender=self.__class__, connection=self) 238 | cursor = self.connection.cursor() 239 | cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None 240 | return CursorWrapper(cursor) 241 | 242 | 243 | class DatabaseWrapper13(OriginalDatabaseWrapper): 244 | ''' 245 | For Django 1.3.x 246 | ''' 247 | set_up_pool_config = _set_up_pool_config 248 | create_connection_pool = _create_connection_pool 249 | 250 | def __init__(self, *args, **kwargs): 251 | super(DatabaseWrapper13, self).__init__(*args, **kwargs) 252 | self.set_up_pool_config() 253 | 254 | def _cursor(self): 255 | ''' 256 | Override _cursor to plug in our connection pool code. We'll return a wrapped Connection 257 | which can handle returning itself to the pool when its .close() method is called. 258 | ''' 259 | from django.db.backends.postgresql.version import get_version 260 | 261 | new_connection = False 262 | set_tz = False 263 | settings_dict = self.settings_dict 264 | 265 | if self.connection is None or connection_pools[self.alias]['settings'] != settings_dict: 266 | new_connection = True 267 | set_tz = settings_dict.get('TIME_ZONE') 268 | 269 | # Is this the initial use of the global connection_pools dictionary for 270 | # this python interpreter? Build a ThreadedConnectionPool instance and 271 | # add it to the dictionary if so. 272 | if self.alias not in connection_pools or connection_pools[self.alias]['settings'] != settings_dict: 273 | if settings_dict['NAME'] == '': 274 | from django.core.exceptions import ImproperlyConfigured 275 | raise ImproperlyConfigured("You need to specify NAME in your Django settings file.") 276 | conn_params = { 277 | 'database': settings_dict['NAME'], 278 | } 279 | conn_params.update(settings_dict['OPTIONS']) 280 | for extra in ['autocommit'] + pool_config_defaults.keys(): 281 | if extra in conn_params: 282 | del conn_params[extra] 283 | if settings_dict['USER']: 284 | conn_params['user'] = settings_dict['USER'] 285 | if settings_dict['PASSWORD']: 286 | conn_params['password'] = settings_dict['PASSWORD'] 287 | if settings_dict['HOST']: 288 | conn_params['host'] = settings_dict['HOST'] 289 | if settings_dict['PORT']: 290 | conn_params['port'] = settings_dict['PORT'] 291 | 292 | self.create_connection_pool(conn_params) 293 | 294 | self.connection = PooledConnection(connection_pools[self.alias]['pool'], 295 | test_query=self._test_on_borrow_query) 296 | self.connection.set_client_encoding('UTF8') 297 | self.connection.set_isolation_level(self.isolation_level) 298 | # We'll continue to emulate the old signal frequency in case any code depends upon it 299 | connection_created.send(sender=self.__class__, connection=self) 300 | cursor = self.connection.cursor() 301 | cursor.tzinfo_factory = None 302 | if new_connection: 303 | if set_tz: 304 | cursor.execute("SET TIME ZONE %s", [settings_dict['TIME_ZONE']]) 305 | if not hasattr(self, '_version'): 306 | self.__class__._version = get_version(cursor) 307 | if self._version[0:2] < (8, 0): 308 | # No savepoint support for earlier version of PostgreSQL. 309 | self.features.uses_savepoints = False 310 | if self.features.uses_autocommit: 311 | if self._version[0:2] < (8, 2): 312 | # FIXME: Needs extra code to do reliable model insert 313 | # handling, so we forbid it for now. 314 | from django.core.exceptions import ImproperlyConfigured 315 | raise ImproperlyConfigured("You cannot use autocommit=True with PostgreSQL prior to 8.2 at the moment.") 316 | else: 317 | # FIXME: Eventually we're enable this by default for 318 | # versions that support it, but, right now, that's hard to 319 | # do without breaking other things (#10509). 320 | self.features.can_return_id_from_insert = True 321 | return CursorWrapper(cursor) 322 | 323 | ''' 324 | Choose a version of the DatabaseWrapper class to use based on the Django 325 | version. This is a bit hacky, what's a more elegant way? 326 | ''' 327 | django_version = get_django_version() 328 | if django_version.startswith('1.3'): 329 | from django.db.backends.postgresql_psycopg2.base import CursorWrapper 330 | 331 | class DatabaseWrapper(DatabaseWrapper13): 332 | pass 333 | elif django_version.startswith('1.4') or django_version.startswith('1.5'): 334 | from django.conf import settings 335 | from django.db.backends.postgresql_psycopg2.base import utc_tzinfo_factory, \ 336 | CursorWrapper 337 | 338 | # The force_str call around the password seems to be the only change from 339 | # 1.4 to 1.5, so we'll use the same DatabaseWrapper class and make 340 | # force_str a no-op. 341 | try: 342 | from django.utils.encoding import force_str 343 | except ImportError: 344 | force_str = lambda x: x 345 | 346 | class DatabaseWrapper(DatabaseWrapper14and15): 347 | pass 348 | elif django_version.startswith('1.6'): 349 | class DatabaseWrapper(DatabaseWrapper16): 350 | pass 351 | else: 352 | raise ImportError("Unsupported Django version %s" % django_version) 353 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from distutils.core import setup 3 | from setuptools import find_packages 4 | 5 | setup( 6 | name='django-db-pool', 7 | version='0.0.10', 8 | author=u'Greg McGuire', 9 | author_email='gregjmcguire+github@gmail.com', 10 | packages=find_packages(), 11 | url='https://github.com/gmcguire/django-db-pool', 12 | license='BSD licence, see LICENSE', 13 | description='Basic database persistance / connection pooling for Django + ' + \ 14 | 'Postgres.', 15 | long_description=open('README.md').read(), 16 | classifiers=[ 17 | 'Topic :: Database', 18 | 'Development Status :: 3 - Alpha', 19 | 'Environment :: Web Environment', 20 | 'Framework :: Django', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Natural Language :: English', 24 | 'Programming Language :: Python', 25 | ], 26 | zip_safe=False, 27 | install_requires=[ 28 | "Django>=1.3,<1.6.99", 29 | "psycopg2>=2.4.2", 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Simplistic test of pooling code 3 | 4 | Created on Mar 28, 2013 5 | 6 | @author: greg 7 | ''' 8 | import random 9 | import threading 10 | 11 | n_threads = 6 12 | n_fast_tests = 1000 13 | n_slow_tests = 10 14 | 15 | def test_slow_connection(execs_remaining): 16 | print '%s: Test slow %s' % (threading.current_thread().name, n_slow_tests - execs_remaining) 17 | 18 | signals.request_started.send(sender=base.BaseHandler) 19 | 20 | cursor = connection.cursor() 21 | cursor.execute("SELECT pg_sleep(1)") 22 | 23 | signals.request_finished.send(sender=base.BaseHandler) 24 | 25 | def test_fast_connection(execs_remaining): 26 | print '%s: Test fast %s' % (threading.current_thread().name, n_fast_tests - execs_remaining) 27 | 28 | signals.request_started.send(sender=base.BaseHandler) 29 | 30 | cursor = connection.cursor() 31 | cursor.execute("SELECT 1") 32 | row = cursor.fetchone() 33 | assert(row[0] == 1) 34 | 35 | signals.request_finished.send(sender=base.BaseHandler) 36 | 37 | def test_connection(): 38 | l_fast_tests = n_fast_tests 39 | l_slow_tests = n_slow_tests 40 | 41 | while l_fast_tests > 0 or l_slow_tests > 0: 42 | if random.randint(0, n_fast_tests + n_slow_tests) < n_slow_tests and l_slow_tests > 0: 43 | test_slow_connection(l_slow_tests) 44 | l_slow_tests -= 1 45 | elif l_fast_tests > 0: 46 | test_fast_connection(l_fast_tests) 47 | l_fast_tests -= 1 48 | 49 | 50 | if __name__ == '__main__': 51 | from django.core import signals 52 | from django.core.handlers import base 53 | from django.db import connection 54 | 55 | print ('Running test_connection in %s threads with %s fast / %s slow loops each. ' 56 | 'Should take about %s seconds.') % (n_threads, n_fast_tests, n_slow_tests, n_slow_tests) 57 | 58 | # Warm up pool 59 | cursor = connection.cursor() 60 | cursor.execute("SELECT 1") 61 | row = cursor.fetchone() 62 | assert(row[0] == 1) 63 | connection.close() 64 | 65 | # Take requests in n_threads 66 | for n in range(n_threads): 67 | t = threading.Thread(target=test_connection) 68 | t.start() 69 | --------------------------------------------------------------------------------