diff options
-rw-r--r-- | oslo_db/tests/__init__.py | 25 | ||||
-rw-r--r-- | oslo_db/tests/sqlalchemy/test_async_eventlet.py | 127 | ||||
-rw-r--r-- | test-requirements.txt | 1 | ||||
-rw-r--r-- | tox.ini | 6 |
4 files changed, 159 insertions, 0 deletions
diff --git a/oslo_db/tests/__init__.py b/oslo_db/tests/__init__.py index e69de29..f080dde 100644 --- a/oslo_db/tests/__init__.py +++ b/oslo_db/tests/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2014 Rackspace +# 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 os + + +def should_run_eventlet_tests(): + return bool(int(os.environ.get('TEST_EVENTLET') or '0')) + + +if should_run_eventlet_tests(): + import eventlet + eventlet.monkey_patch() diff --git a/oslo_db/tests/sqlalchemy/test_async_eventlet.py b/oslo_db/tests/sqlalchemy/test_async_eventlet.py new file mode 100644 index 0000000..58e4787 --- /dev/null +++ b/oslo_db/tests/sqlalchemy/test_async_eventlet.py @@ -0,0 +1,127 @@ +# Copyright (c) 2014 Rackspace Hosting +# 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. + +"""Unit tests for SQLAlchemy and eventlet interaction.""" + +import logging +import unittest2 + +from oslo_utils import importutils +import sqlalchemy as sa +from sqlalchemy.ext import declarative as sa_decl + +from oslo_db import exception as db_exc +from oslo_db.sqlalchemy import models +from oslo_db.sqlalchemy import test_base +from oslo_db import tests + + +class EventletTestMixin(object): + def setUp(self): + super(EventletTestMixin, self).setUp() + + BASE = sa_decl.declarative_base() + + class TmpTable(BASE, models.ModelBase): + __tablename__ = 'test_async_eventlet' + id = sa.Column('id', sa.Integer, primary_key=True, nullable=False) + foo = sa.Column('foo', sa.Integer) + __table_args__ = ( + sa.UniqueConstraint('foo', name='uniq_foo'), + ) + + self.test_table = TmpTable + TmpTable.__table__.create(self.engine) + self.addCleanup(lambda: TmpTable.__table__.drop(self.engine)) + + @unittest2.skipIf(not tests.should_run_eventlet_tests(), + 'eventlet tests disabled unless TEST_EVENTLET=1') + def test_concurrent_transaction(self): + # Cause sqlalchemy to log executed SQL statements. Useful to + # determine exactly what and when was sent to DB. + sqla_logger = logging.getLogger('sqlalchemy.engine') + sqla_logger.setLevel(logging.INFO) + self.addCleanup(sqla_logger.setLevel, logging.NOTSET) + + def operate_on_row(name, ready=None, proceed=None): + logging.debug('%s starting', name) + _session = self.sessionmaker() + with _session.begin(): + logging.debug('%s ready', name) + + # Modify the same row, inside transaction + tbl = self.test_table() + tbl.update({'foo': 10}) + tbl.save(_session) + + if ready is not None: + ready.send() + if proceed is not None: + logging.debug('%s waiting to proceed', name) + proceed.wait() + logging.debug('%s exiting transaction', name) + logging.debug('%s terminating', name) + return True + + eventlet = importutils.try_import('eventlet') + if eventlet is None: + return self.skip('eventlet is required for this test') + + a_ready = eventlet.event.Event() + a_proceed = eventlet.event.Event() + b_proceed = eventlet.event.Event() + + # thread A opens transaction + logging.debug('spawning A') + a = eventlet.spawn(operate_on_row, 'A', + ready=a_ready, proceed=a_proceed) + logging.debug('waiting for A to enter transaction') + a_ready.wait() + + # thread B opens transaction on same row + logging.debug('spawning B') + b = eventlet.spawn(operate_on_row, 'B', + proceed=b_proceed) + logging.debug('waiting for B to (attempt to) enter transaction') + eventlet.sleep(1) # should(?) advance B to blocking on transaction + + # While B is still blocked, A should be able to proceed + a_proceed.send() + + # Will block forever(*) if DB library isn't reentrant. + # (*) Until some form of timeout/deadlock detection kicks in. + # This is the key test that async is working. If this hangs + # (or raises a timeout/deadlock exception), then you have failed + # this test. + self.assertTrue(a.wait()) + + b_proceed.send() + # If everything proceeded without blocking, B will throw a + # "duplicate entry" exception when it tries to insert the same row + self.assertRaises(db_exc.DBDuplicateEntry, b.wait) + + +# Note that sqlite fails the above concurrency tests, and is not +# mentioned below. +# ie: This file performs no tests by default. + +class MySQLEventletTestCase(EventletTestMixin, + test_base.MySQLOpportunisticTestCase): + pass + + +class PostgreSQLEventletTestCase(EventletTestMixin, + test_base.PostgreSQLOpportunisticTestCase): + pass diff --git a/test-requirements.txt b/test-requirements.txt index 210c817..974fde4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,6 +7,7 @@ hacking<0.11,>=0.10.0 coverage>=3.6 discover doc8 # Apache-2.0 +eventlet>=0.17.4 fixtures>=1.3.1 PyMySQL>=0.6.2 # MIT License psycopg2 @@ -10,6 +10,7 @@ envlist = py26,py27,py34,pep8,pip-missing-reqs # for oslo libraries because of the namespace package. #usedevelop = True whitelist_externals = bash + env install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} @@ -21,6 +22,11 @@ commands = bash tools/pretty_tox.sh '{posargs}' commands = pip install SQLAlchemy>=0.9.0,!=0.9.5,<1.0.0 python setup.py testr --slowest --testr-args='{posargs}' +[testenv:py27] +commands = + env TEST_EVENTLET=0 bash tools/pretty_tox.sh '{posargs}' + env TEST_EVENTLET=1 bash tools/pretty_tox.sh '{posargs}' + [testenv:mysql-python] setenv = {[testenv]setenv} |