summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey Kurilin <akurilin@mirantis.com>2014-05-29 12:56:08 +0300
committerAndrey Kurilin <akurilin@mirantis.com>2014-06-06 18:16:00 +0300
commitf3ece0bb025c9d16d305fba7c861ce30dc3daf23 (patch)
tree03edd1f411693dfaf45f033f7d6a3ae03bd58545
parentfad6e5c2302811e8721123fa8e7f43a5bdecb068 (diff)
downloadoslo-db-f3ece0bb025c9d16d305fba7c861ce30dc3daf23.tar.gz
Add eventlet.tpool.Proxy for DB API calls
Ability to use thread pooling for DB API calls should be returned in oslo.db as DB API wrapper. Base on the fact that this wrapper is optional, `eventlet` should not be added in requirements. bp add-tpool-proxy-wrapper Change-Id: I2343556c157e9f0f695e14ca0283914ef23c972c
-rw-r--r--oslo/db/concurrency.py81
-rw-r--r--setup.cfg1
-rw-r--r--tests/test_concurrency.py108
3 files changed, 190 insertions, 0 deletions
diff --git a/oslo/db/concurrency.py b/oslo/db/concurrency.py
new file mode 100644
index 0000000..5134785
--- /dev/null
+++ b/oslo/db/concurrency.py
@@ -0,0 +1,81 @@
+# Copyright 2014 Mirantis.inc
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+import logging
+import threading
+
+from oslo.config import cfg
+
+from oslo.db import api
+from oslo.db.openstack.common.gettextutils import _LE
+
+
+LOG = logging.getLogger(__name__)
+
+tpool_opts = [
+ cfg.BoolOpt('use_tpool',
+ default=False,
+ deprecated_name='dbapi_use_tpool',
+ deprecated_group='DEFAULT',
+ help='Enable the experimental use of thread pooling for '
+ 'all DB API calls'),
+]
+
+
+class TpoolDbapiWrapper(object):
+ """DB API wrapper class.
+
+ This wraps the oslo DB API with an option to be able to use eventlet's
+ thread pooling. Since the CONF variable may not be loaded at the time
+ this class is instantiated, we must look at it on the first DB API call.
+ """
+
+ def __init__(self, conf, backend_mapping):
+ self._db_api = None
+ self._backend_mapping = backend_mapping
+ self._conf = conf
+ self._conf.register_opts(tpool_opts, 'database')
+ self._lock = threading.Lock()
+
+ @property
+ def _api(self):
+ if not self._db_api:
+ with self._lock:
+ if not self._db_api:
+ db_api = api.DBAPI.from_config(
+ conf=self._conf, backend_mapping=self._backend_mapping)
+ if self._conf.database.use_tpool:
+ try:
+ from eventlet import tpool
+ except ImportError:
+ LOG.exception(_LE("'eventlet' is required for "
+ "TpoolDbapiWrapper."))
+ raise
+ self._db_api = tpool.Proxy(db_api)
+ else:
+ self._db_api = db_api
+ return self._db_api
+
+ def __getattr__(self, key):
+ return getattr(self._api, key)
+
+
+def list_opts():
+ """Returns a list of oslo.config options available in this module.
+
+ :returns: a list of (group_name, opts) tuples
+ """
+ return [('database', copy.deepcopy(tpool_opts))]
diff --git a/setup.cfg b/setup.cfg
index f393ea5..3062d81 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,6 +27,7 @@ namespace_packages =
[entry_points]
oslo.config.opts =
oslo.db = oslo.db.options:list_opts
+ oslo.db.concurrency = oslo.db.concurrency:list_opts
oslo.db.migration =
alembic = oslo.db.sqlalchemy.migration_cli.ext_alembic:AlembicExtension
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
new file mode 100644
index 0000000..cf34bba
--- /dev/null
+++ b/tests/test_concurrency.py
@@ -0,0 +1,108 @@
+# Copyright 2014 Mirantis.inc
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import sys
+
+import mock
+
+from oslo.db import concurrency
+from tests import utils as test_utils
+
+FAKE_BACKEND_MAPPING = {'sqlalchemy': 'fake.db.sqlalchemy.api'}
+
+
+class TpoolDbapiWrapperTestCase(test_utils.BaseTestCase):
+
+ def setUp(self):
+ super(TpoolDbapiWrapperTestCase, self).setUp()
+ self.db_api = concurrency.TpoolDbapiWrapper(
+ conf=self.conf, backend_mapping=FAKE_BACKEND_MAPPING)
+
+ # NOTE(akurilin): We are not going to add `eventlet` to `oslo.db` in
+ # requirements (`requirements.txt` and `test-requirements.txt`) due to
+ # the following reasons:
+ # - supporting of eventlet's thread pooling is totally optional;
+ # - we don't need to test `tpool.Proxy` functionality itself,
+ # because it's a tool from the third party library;
+ # - `eventlet` would prevent us from running unit tests on Python 3.x
+ # versions, because it doesn't support them yet.
+ #
+ # As we don't test `tpool.Proxy`, we can safely mock it in tests.
+
+ self.proxy = mock.MagicMock()
+ self.eventlet = mock.MagicMock()
+ self.eventlet.tpool.Proxy.return_value = self.proxy
+ sys.modules['eventlet'] = self.eventlet
+ self.addCleanup(sys.modules.pop, 'eventlet', None)
+
+ @mock.patch('oslo.db.api.DBAPI')
+ def test_db_api_common(self, mock_db_api):
+ # test context:
+ # CONF.database.use_tpool == False
+ # eventlet is installed
+ # expected result:
+ # TpoolDbapiWrapper should wrap DBAPI
+
+ fake_db_api = mock.MagicMock()
+ mock_db_api.from_config.return_value = fake_db_api
+
+ # get access to some db-api method
+ self.db_api.fake_call_1
+
+ mock_db_api.from_config.assert_called_once_with(
+ conf=self.conf, backend_mapping=FAKE_BACKEND_MAPPING)
+ self.assertEqual(self.db_api._db_api, fake_db_api)
+ self.assertFalse(self.eventlet.tpool.Proxy.called)
+
+ # get access to other db-api method to be sure that api didn't changed
+ self.db_api.fake_call_2
+
+ self.assertEqual(self.db_api._db_api, fake_db_api)
+ self.assertFalse(self.eventlet.tpool.Proxy.called)
+ self.assertEqual(1, mock_db_api.from_config.call_count)
+
+ @mock.patch('oslo.db.api.DBAPI')
+ def test_db_api_config_change(self, mock_db_api):
+ # test context:
+ # CONF.database.use_tpool == True
+ # eventlet is installed
+ # expected result:
+ # TpoolDbapiWrapper should wrap tpool proxy
+
+ fake_db_api = mock.MagicMock()
+ mock_db_api.from_config.return_value = fake_db_api
+ self.conf.set_override('use_tpool', True, group='database')
+
+ # get access to some db-api method
+ self.db_api.fake_call
+
+ # CONF.database.use_tpool is True, so we get tpool proxy in this case
+ mock_db_api.from_config.assert_called_once_with(
+ conf=self.conf, backend_mapping=FAKE_BACKEND_MAPPING)
+ self.eventlet.tpool.Proxy.assert_called_once_with(fake_db_api)
+ self.assertEqual(self.db_api._db_api, self.proxy)
+
+ @mock.patch('oslo.db.api.DBAPI')
+ def test_db_api_without_installed_eventlet(self, mock_db_api):
+ # test context:
+ # CONF.database.use_tpool == True
+ # eventlet is not installed
+ # expected result:
+ # raise ImportError
+
+ self.conf.set_override('use_tpool', True, group='database')
+ del sys.modules['eventlet']
+
+ self.assertRaises(ImportError, getattr, self.db_api, 'fake')