Skip to content

Commit a643d98

Browse files
committed
INTPYTHON-574 Add support for connection pooling
1 parent ad165bd commit a643d98

File tree

8 files changed

+108
-5
lines changed

8 files changed

+108
-5
lines changed

django_mongodb_backend/base.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
8989
"endswith": "LIKE '%%' || {}",
9090
"iendswith": "LIKE '%%' || UPPER({})",
9191
}
92+
_connection_pools = {}
9293

9394
def _isnull_operator(a, b):
9495
is_null = {
@@ -176,7 +177,12 @@ def get_connection_params(self):
176177

177178
@async_unsafe
178179
def get_new_connection(self, conn_params):
179-
return MongoClient(**conn_params, driver=self._driver_info())
180+
if self.alias not in self._connection_pools:
181+
conn = MongoClient(**conn_params, driver=self._driver_info())
182+
# setdefault() ensures that multiple threads don't set this in
183+
# parallel.
184+
self._connection_pools.setdefault(self.alias, conn)
185+
return self._connection_pools[self.alias]
180186

181187
def _driver_info(self):
182188
if not os.environ.get("RUNNING_DJANGOS_TEST_SUITE"):
@@ -192,11 +198,28 @@ def _rollback(self):
192198
def set_autocommit(self, autocommit, force_begin_transaction_with_broken_autocommit=False):
193199
self.autocommit = autocommit
194200

201+
def _close(self):
202+
# Normally called by close(), this method is also called by some tests.
203+
pass
204+
195205
@async_unsafe
196206
def close(self):
197-
super().close()
207+
self.validate_thread_sharing()
208+
# MongoClient is a connection pool and, unlike database drivers that
209+
# implement PEP 249, shouldn't be closed by connection.close().
210+
211+
def close_pool(self):
212+
"""Close the MongoClient."""
213+
connection = self.connection
214+
if connection is None:
215+
return
216+
# Remove all references to the connection.
217+
self.connection = None
198218
with contextlib.suppress(AttributeError):
199219
del self.database
220+
del self._connection_pools[self.alias]
221+
# Then close it.
222+
connection.close()
200223

201224
@async_unsafe
202225
def cursor(self):

django_mongodb_backend/creation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
class DatabaseCreation(BaseDatabaseCreation):
1010
def _execute_create_test_db(self, cursor, parameters, keepdb=False):
11+
# Close the connection (which may point to the non-test database) so
12+
# that a new connection to the test database can be established later.
13+
self.connection.close_pool()
1114
if not keepdb:
1215
self._destroy_test_db(parameters["dbname"], verbosity=0)
1316

@@ -29,3 +32,8 @@ def create_test_db(self, *args, **kwargs):
2932
database=self.connection.alias, verbosity=kwargs["verbosity"]
3033
)
3134
return test_database_name
35+
36+
def destroy_test_db(self, old_database_name=None, verbosity=1, keepdb=False, suffix=None):
37+
super().destroy_test_db(old_database_name, verbosity, keepdb, suffix)
38+
# Close the connection to the test database.
39+
self.connection.close_pool()

django_mongodb_backend/features.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
7272
# Connection creation doesn't follow the usual Django API.
7373
"backends.tests.ThreadTests.test_pass_connection_between_threads",
7474
"backends.tests.ThreadTests.test_default_connection_thread_local",
75-
"test_utils.tests.DisallowedDatabaseQueriesTests.test_disallowed_thread_database_connection",
7675
# Object of type ObjectId is not JSON serializable.
7776
"auth_tests.test_views.LoginTest.test_login_session_without_hash_session_key",
7877
# GenericRelation.value_to_string() assumes integer pk.
@@ -544,6 +543,19 @@ def django_test_expected_failures(self):
544543
"custom_lookups.tests.LookupTests.test_div3_extract",
545544
"custom_lookups.tests.SubqueryTransformTests.test_subquery_usage",
546545
},
546+
"connection.close() does not close the connection.": {
547+
"servers.test_liveserverthread.LiveServerThreadTest.test_closes_connections",
548+
"servers.tests.LiveServerTestCloseConnectionTest.test_closes_connections",
549+
},
550+
"Disallowed query protection doesn't work on MongoDB.": {
551+
# Because this backend doesn't use cursor(), chunked_cursor(), etc.
552+
# https://github.com/django/django/blob/045110ff3089aefd9c3e65c707df465bacfed986/django/test/testcases.py#L195-L206
553+
"test_utils.test_testcase.TestTestCase.test_disallowed_database_queries",
554+
"test_utils.test_transactiontestcase.DisallowedDatabaseQueriesTests.test_disallowed_database_queries",
555+
"test_utils.tests.DisallowedDatabaseQueriesTests.test_disallowed_database_chunked_cursor_queries",
556+
"test_utils.tests.DisallowedDatabaseQueriesTests.test_disallowed_database_queries",
557+
"test_utils.tests.DisallowedDatabaseQueriesTests.test_disallowed_thread_database_connection",
558+
},
547559
}
548560

549561
@cached_property

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Models
4040
- :doc:`ref/models/querysets`
4141
- :doc:`ref/models/models`
4242
- :doc:`ref/models/indexes`
43+
- :doc:`ref/database`
4344

4445
**Topic guides:**
4546

docs/source/ref/database.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
==================
2+
Database reference
3+
==================
4+
5+
This document supplements :doc:`Django's documentation on databases
6+
<django:ref/databases>`.
7+
8+
Persistent connections
9+
======================
10+
11+
Persistent connections avoid the overhead of reestablishing a connection to
12+
the database in each HTTP request. They're normally controlled by the
13+
:setting:`CONN_MAX_AGE` parameter which defines the maximum lifetime of a
14+
connection. However, this parameter is unnecessary and has no effect with
15+
Django MongoDB Backend because Django's API for connection-closing
16+
(``django.db.connection.close()``) has no effect. In other words, persistent
17+
connections are enabled by default.
18+
19+
.. versionadded:: 5.2.0b0
20+
21+
Support for connection pooling was added. In older versions, use
22+
:setting:`CONN_MAX_AGE` to enable persistent connections.
23+
24+
.. _connection-management:
25+
26+
Connection management
27+
=====================
28+
29+
Django uses this backend to open a connection pool to the database when it
30+
first makes a database query. It keeps this pool open and reuses it in
31+
subsequent requests.
32+
33+
The underlying :class:`~pymongo.mongo_client.MongoClient` takes care connection
34+
management, so the :setting:`CONN_HEALTH_CHECKS` setting is unnecessary and has
35+
no effect.
36+
37+
Django's API for connection-closing (``django.db.connection.close()``) has no
38+
effect. Rather, if you need to close the connection pool, use
39+
``django.db.connection.close_pool()``.
40+
41+
.. versionadded:: 5.2.0b0
42+
43+
Support for connection pooling and ``connection.close_pool()`` were added.

docs/source/ref/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ API reference
77

88
models/index
99
forms
10+
database
1011
django-admin
1112
utils

docs/source/releases/5.2.x.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ New features
2020

2121
- Added :class:`.SearchIndex` and :class:`.VectorSearchIndex` for use on
2222
a model's :attr:`Meta.indexes <django.db.models.Options.indexes>`.
23+
- PyMongo's connection pooling is now used by default. See
24+
:ref:`connection-management`.
2325

2426
Backwards incompatible changes
2527
------------------------------

tests/backend_/test_base.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ def test_set_autocommit(self):
2323
connection.set_autocommit(True)
2424
self.assertIs(connection.get_autocommit(), True)
2525

26+
def test_close(self):
27+
"""connection.close() doesn't close the connection."""
28+
conn = connection.connection
29+
self.assertIsNotNone(conn)
30+
connection.close()
31+
self.assertEqual(connection.connection, conn)
32+
33+
def test_close_pool(self):
34+
"""connection.close_pool() closes the connection."""
35+
self.assertIsNotNone(connection.connection)
36+
connection.close_pool()
37+
self.assertIsNone(connection.connection)
38+
2639
def test_connection_created_database_attr(self):
2740
"""
2841
connection.database is available in the connection_created signal.
@@ -33,11 +46,11 @@ def receiver(sender, connection, **kwargs): # noqa: ARG001
3346
data["database"] = connection.database
3447

3548
connection_created.connect(receiver)
36-
connection.close()
49+
connection.close_pool()
3750
# Accessing database implicitly connects.
3851
connection.database # noqa: B018
3952
self.assertIs(data["database"], connection.database)
40-
connection.close()
53+
connection.close_pool()
4154
connection_created.disconnect(receiver)
4255
data.clear()
4356
connection.connect()

0 commit comments

Comments
 (0)