1
0
mirror of https://github.com/django/django.git synced 2024-12-22 09:05:43 +00:00

Fixed #7732 -- Added support for connection pools on Oracle.

This commit is contained in:
suraj 2024-09-10 20:56:16 +05:30 committed by Sarah Boyce
parent 2249370c86
commit 0d9872fc9a
7 changed files with 215 additions and 9 deletions

View File

@ -14,6 +14,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.oracle.oracledb_any import is_oracledb
from django.db.backends.utils import debug_transaction
from django.utils.asyncio import async_unsafe
from django.utils.encoding import force_bytes, force_str
@ -235,6 +236,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
introspection_class = DatabaseIntrospection
ops_class = DatabaseOperations
validation_class = DatabaseValidation
_connection_pools = {}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -243,10 +245,52 @@ class DatabaseWrapper(BaseDatabaseWrapper):
)
self.features.can_return_columns_from_insert = use_returning_into
@property
def is_pool(self):
return self.settings_dict["OPTIONS"].get("pool", False)
@property
def pool(self):
if not self.is_pool:
return None
if self.settings_dict.get("CONN_MAX_AGE", 0) != 0:
raise ImproperlyConfigured(
"Pooling doesn't support persistent connections."
)
pool_key = (self.alias, self.settings_dict["USER"])
if pool_key not in self._connection_pools:
connect_kwargs = self.get_connection_params()
pool_options = connect_kwargs.pop("pool")
if pool_options is not True:
connect_kwargs.update(pool_options)
pool = Database.create_pool(
user=self.settings_dict["USER"],
password=self.settings_dict["PASSWORD"],
dsn=dsn(self.settings_dict),
**connect_kwargs,
)
self._connection_pools.setdefault(pool_key, pool)
return self._connection_pools[pool_key]
def close_pool(self):
if self.pool:
self.pool.close(force=True)
pool_key = (self.alias, self.settings_dict["USER"])
del self._connection_pools[pool_key]
def get_database_version(self):
return self.oracle_version
def get_connection_params(self):
# Pooling feature is only supported for oracledb.
if self.is_pool and not is_oracledb:
raise ImproperlyConfigured(
"Pooling isn't supported by cx_Oracle. Use python-oracledb instead."
)
conn_params = self.settings_dict["OPTIONS"].copy()
if "use_returning_into" in conn_params:
del conn_params["use_returning_into"]
@ -254,6 +298,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
@async_unsafe
def get_new_connection(self, conn_params):
if self.pool:
return self.pool.acquire()
return Database.connect(
user=self.settings_dict["USER"],
password=self.settings_dict["PASSWORD"],
@ -345,6 +391,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
else:
return True
def close_if_health_check_failed(self):
if self.pool:
# The pool only returns healthy connections.
return
return super().close_if_health_check_failed()
@cached_property
def oracle_version(self):
with self.temporary_connection():

View File

@ -205,13 +205,15 @@ class DatabaseCreation(BaseDatabaseCreation):
Destroy a test database, prompting the user for confirmation if the
database already exists. Return the name of the test database created.
"""
self.connection.settings_dict["USER"] = self.connection.settings_dict[
"SAVED_USER"
]
self.connection.settings_dict["PASSWORD"] = self.connection.settings_dict[
"SAVED_PASSWORD"
]
if not self.connection.is_pool:
self.connection.settings_dict["USER"] = self.connection.settings_dict[
"SAVED_USER"
]
self.connection.settings_dict["PASSWORD"] = self.connection.settings_dict[
"SAVED_PASSWORD"
]
self.connection.close()
self.connection.close_pool()
parameters = self._get_test_db_params()
with self._maindb_connection.cursor() as cursor:
if self._test_user_create():
@ -223,6 +225,7 @@ class DatabaseCreation(BaseDatabaseCreation):
self.log("Destroying test database tables...")
self._execute_test_db_destruction(cursor, parameters, verbosity)
self._maindb_connection.close()
self._maindb_connection.close_pool()
def _execute_test_db_creation(self, cursor, parameters, verbosity, keepdb=False):
if verbosity >= 2:

View File

@ -139,6 +139,25 @@ class DatabaseFeatures(BaseDatabaseFeatures):
},
}
)
if self.connection.is_pool:
skips.update(
{
"Pooling does not support persistent connections": {
"backends.base.test_base.ConnectionHealthChecksTests."
"test_health_checks_enabled",
"backends.base.test_base.ConnectionHealthChecksTests."
"test_health_checks_enabled_errors_occurred",
"backends.base.test_base.ConnectionHealthChecksTests."
"test_health_checks_disabled",
"backends.base.test_base.ConnectionHealthChecksTests."
"test_set_autocommit_health_checks_enabled",
"servers.tests.LiveServerTestCloseConnectionTest."
"test_closes_connections",
"backends.oracle.tests.TransactionalTests."
"test_password_with_at_sign",
},
}
)
if is_oracledb and self.connection.oracledb_version >= (2, 1, 2):
skips.update(
{

View File

@ -994,7 +994,7 @@ Oracle notes
============
Django supports `Oracle Database Server`_ versions 19c and higher. Version
1.3.2 or higher of the `oracledb`_ Python driver is required.
2.3.0 or higher of the `oracledb`_ Python driver is required.
.. deprecated:: 5.0
@ -1105,6 +1105,46 @@ Example of a full DSN string::
"(CONNECT_DATA=(SERVICE_NAME=orclpdb1)))"
)
.. _oracle-pool:
Connection pool
---------------
.. versionadded:: 5.2
To use a connection pool with `oracledb`_, set ``"pool"`` to ``True`` in the
:setting:`OPTIONS` part of your database configuration. This uses the driver's
`create_pool()`_ default values::
DATABASES = {
"default": {
"ENGINE": "django.db.backends.oracle",
# ...
"OPTIONS": {
"pool": True,
},
},
}
To pass custom parameters to the driver's `create_pool()`_ function, you can
alternatively set ``"pool"`` to be a dict::
DATABASES = {
"default": {
"ENGINE": "django.db.backends.oracle",
# ...
"OPTIONS": {
"pool": {
"min": 1,
"max": 10,
# ...
}
},
},
}
.. _`create_pool()`: https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html#connection-pooling
Threaded option
---------------

View File

@ -197,6 +197,9 @@ Database backends
instead of ``utf8``, which is an alias for the deprecated character set
``utf8mb3``.
* Oracle backends now support :ref:`connection pools <oracle-pool>`, by setting
``"pool"`` in the :setting:`OPTIONS` part of your database configuration.
Decorators
~~~~~~~~~~
@ -464,6 +467,9 @@ Miscellaneous
* :meth:`.QuerySet.update_or_create`
* :meth:`.QuerySet.aupdate_or_create`
* The minimum supported version of ``oracledb`` is increased from 1.3.2 to
2.3.0.
.. _deprecated-features-5.2:
Features deprecated in 5.2

View File

@ -1,12 +1,28 @@
import copy
import unittest
from unittest import mock
from django.db import DatabaseError, NotSupportedError, connection
from django.core.exceptions import ImproperlyConfigured
from django.db import DatabaseError, NotSupportedError, ProgrammingError, connection
from django.db.models import BooleanField
from django.test import TestCase, TransactionTestCase
from ..models import Square, VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
try:
from django.db.backends.oracle.oracledb_any import is_oracledb
except ImportError:
is_oracledb = False
def no_pool_connection(alias=None):
new_connection = connection.copy(alias)
new_connection.settings_dict = copy.deepcopy(connection.settings_dict)
# Ensure that the second connection circumvents the pool, this is kind
# of a hack, but we cannot easily change the pool connections.
new_connection.settings_dict["OPTIONS"]["pool"] = False
return new_connection
@unittest.skipUnless(connection.vendor == "oracle", "Oracle tests")
class Tests(TestCase):
@ -69,6 +85,76 @@ class Tests(TestCase):
connection.check_database_version_supported()
self.assertTrue(mocked_get_database_version.called)
@unittest.skipUnless(is_oracledb, "Pool specific tests")
def test_pool_set_to_true(self):
new_connection = no_pool_connection(alias="default_pool")
new_connection.settings_dict["OPTIONS"]["pool"] = True
try:
self.assertIsNotNone(new_connection.pool)
finally:
new_connection.close_pool()
@unittest.skipUnless(is_oracledb, "Pool specific tests")
def test_pool_reuse(self):
new_connection = no_pool_connection(alias="default_pool")
new_connection.settings_dict["OPTIONS"]["pool"] = {
"min": 0,
"max": 2,
}
self.assertIsNotNone(new_connection.pool)
connections = []
def get_connection():
# copy() reuses the existing alias and as such the same pool.
conn = new_connection.copy()
conn.connect()
connections.append(conn)
return conn
try:
connection_1 = get_connection() # First connection.
get_connection() # Get the second connection.
sql = "select sys_context('userenv', 'sid') from dual"
sids = [conn.cursor().execute(sql).fetchone()[0] for conn in connections]
connection_1.close() # Release back to the pool.
connection_3 = get_connection()
sid = connection_3.cursor().execute(sql).fetchone()[0]
# Reuses the first connection as it is available.
self.assertEqual(sid, sids[0])
finally:
# Release all connections back to the pool.
for conn in connections:
conn.close()
new_connection.close_pool()
@unittest.skipUnless(is_oracledb, "Pool specific tests")
def test_cannot_open_new_connection_in_atomic_block(self):
new_connection = no_pool_connection(alias="default_pool")
new_connection.settings_dict["OPTIONS"]["pool"] = True
msg = "Cannot open a new connection in an atomic block."
new_connection.in_atomic_block = True
new_connection.closed_in_transaction = True
with self.assertRaisesMessage(ProgrammingError, msg):
new_connection.ensure_connection()
@unittest.skipUnless(is_oracledb, "Pool specific tests")
def test_pooling_not_support_persistent_connections(self):
new_connection = no_pool_connection(alias="default_pool")
new_connection.settings_dict["OPTIONS"]["pool"] = True
new_connection.settings_dict["CONN_MAX_AGE"] = 10
msg = "Pooling doesn't support persistent connections."
with self.assertRaisesMessage(ImproperlyConfigured, msg):
new_connection.pool
@unittest.skipIf(is_oracledb, "cx_oracle specific tests")
def test_cx_Oracle_not_support_pooling(self):
new_connection = no_pool_connection()
new_connection.settings_dict["OPTIONS"]["pool"] = True
msg = "Pooling isn't supported by cx_Oracle. Use python-oracledb instead."
with self.assertRaisesMessage(ImproperlyConfigured, msg):
new_connection.connect()
@unittest.skipUnless(connection.vendor == "oracle", "Oracle tests")
class TransactionalTests(TransactionTestCase):

View File

@ -1 +1 @@
oracledb >= 1.3.2
oracledb >= 2.3.0