oslo.db/oslo_db/tests/sqlalchemy/test_async_eventlet.py

128 lines
4.7 KiB
Python
Raw Normal View History

# 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 import tests
Enhanced fixtures for enginefacade-based provisioning The original idea of enginefacade was that applications would call upon the global _TransactionContextManager given in oslo_db.sqlalchemy.enginefacade. However, as it turns out, virtually no Openstack projects seem to be using that technique, and instead, everyone is creating their own ad-hoc _TransactionContextManager objects and usually establishing it as a module-level global. Nova has two of them. Additionally, projects add configuration to these enginefacades (which IS part of the original idea), and this configuration in some cases is necessary to be present for tests that run as well, a key example being the sqlite_fks flag. The original DbFixture integration provided no way of reusing this configuration. Finally, projects very much tend to use custom fixtures in order to define their database communication. Test classes themselves don't really make use of oslo_db's DbTestCase anymore. This patch introduces a modernized fixture system which, in conjunction with the recent provisioning patch, addresses these use cases. Applications will typically create their own subclasses of these fixtures up front to suit the various testing cases they have, including SQLite fixed, SQLite ad-hoc, and opportunistic. In order to accommodate the fixture-based flow along with the use of testresources for opportunistic database provisioning, a mixin class OpportunisticDbTestMixin is still needed when a test needs to use "opportunistic" testing in order to provide the .resources attribute. The calculation of .resources is moved into the fixture system, but because this attribute is consulted before setUp(), the "opportunistic" fixture must be created early and stored. Closes-Bug: #1548960 Change-Id: I0163e637ffef6d45d2573ebe29b5438911d01fce
2016-08-04 17:18:50 -04:00
from oslo_db.tests.sqlalchemy import base as test_base
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.skipTest('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