Merge "Sync SQLAlchemy lifecycle to request cycle"

This commit is contained in:
Jenkins
2014-10-02 19:22:41 +00:00
committed by Gerrit Code Review
6 changed files with 322 additions and 123 deletions
+15 -3
View File
@@ -36,6 +36,7 @@ from barbican.api.controllers import secrets
from barbican.api.controllers import transportkeys
from barbican.api.controllers import versions
from barbican.common import config
from barbican.model import repositories
from barbican.openstack.common import log
from barbican import queue
@@ -63,7 +64,17 @@ class PecanAPI(pecan.Pecan):
performance_controller = performance.PerformanceController()
def __init__(self, *args, **kwargs):
kwargs.setdefault('hooks', []).append(JSONErrorHook())
hooks = [JSONErrorHook()]
if kwargs.pop('is_transactional', None):
transaction_hook = pecan.hooks.TransactionHook(
repositories.start,
repositories.start_read_only,
repositories.commit,
repositories.rollback,
repositories.clear
)
hooks.append(transaction_hook)
kwargs['hooks'] = hooks
super(PecanAPI, self).__init__(*args, **kwargs)
def route(self, req, node, path):
@@ -95,7 +106,7 @@ def create_main_app(global_config, **local_conf):
# Queuing initialization
CONF = cfg.CONF
queue.init(CONF)
queue.init(CONF, is_server_side=False)
class RootController(object):
secrets = secrets.SecretsController()
@@ -103,7 +114,8 @@ def create_main_app(global_config, **local_conf):
containers = containers.ContainersController()
transport_keys = transportkeys.TransportKeysController()
wsgi_app = PecanAPI(RootController(), force_canonical=False)
wsgi_app = PecanAPI(
RootController(), is_transactional=True, force_canonical=False)
if newrelic_loaded:
wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app)
return wsgi_app
+153 -118
View File
@@ -72,6 +72,51 @@ _CONNECTION = None
_IDLE_TIMEOUT = None
def start():
"""Start database and establish a read/write connection to it.
Typically performed at the start of a request cycle, say for POST or PUT
requests.
"""
configure_db()
get_session()
def start_read_only():
"""Start database and establish a read-only connection to it.
Typically performed at the start of a request cycle, say for GET or HEAD
requests.
"""
# TODO(john-wood-w) Add optional, separate engine/connection for reads.
start()
def commit():
"""Commit session state so far to the database.
Typically performed at the end of a request cycle.
"""
get_session().commit()
def rollback():
"""Rollback session state so far.
Typically performed when the request cycle raises an Exception.
"""
get_session().rollback()
def clear():
"""Dispose of this session, releases database resources.
Typically performed at the end of a request cycle, after a
commit() or rollback().
"""
_MAKER.remove()
def setup_db_env():
"""Setup configuration for database."""
global sa_logger, _IDLE_TIMEOUT, _MAX_RETRIES, _RETRY_INTERVAL, _CONNECTION
@@ -96,12 +141,12 @@ def configure_db():
get_engine()
def get_session(autocommit=True, expire_on_commit=False):
def get_session():
"""Helper method to grab session."""
global _MAKER
if not _MAKER:
get_engine()
get_maker(autocommit, expire_on_commit)
get_maker()
assert(_MAKER)
session = _MAKER()
return session
@@ -167,7 +212,7 @@ def get_engine():
return _ENGINE
def get_maker(autocommit=True, expire_on_commit=False):
def get_maker():
"""Return a SQLAlchemy sessionmaker."""
"""May assign __MAKER if not already assigned"""
global _MAKER, _ENGINE
@@ -176,9 +221,7 @@ def get_maker(autocommit=True, expire_on_commit=False):
# Utilize SQLAlchemy's scoped_session to ensure that we only have one
# session instance per thread.
_MAKER = sqlalchemy.orm.scoped_session(
sa_orm.sessionmaker(bind=_ENGINE,
autocommit=autocommit,
expire_on_commit=expire_on_commit))
sa_orm.sessionmaker(bind=_ENGINE))
return _MAKER
@@ -296,10 +339,6 @@ class BaseRepo(object):
configuration.
"""
def __init__(self):
LOG.debug("BaseRepo init...")
configure_db()
def get_session(self, session=None):
LOG.debug("Getting session...")
return session or get_session()
@@ -309,6 +348,7 @@ class BaseRepo(object):
suppress_exception=False, session=None):
"""Get an entity or raise if it does not exist."""
session = self.get_session(session)
try:
query = self._do_build_get_query(entity_id,
keystone_id, session)
@@ -342,27 +382,26 @@ class BaseRepo(object):
raise exception.Invalid(msg)
LOG.debug("Begin create from...")
session = get_session(session)
with session.begin():
# Validate the attributes before we go any further. From my
# (unknown Glance developer) investigation, the @validates
# decorator does not validate
# on new records, only on existing records, which is, well,
# idiotic.
values = self._do_validate(entity.to_dict())
# Validate the attributes before we go any further. From my
# (unknown Glance developer) investigation, the @validates
# decorator does not validate
# on new records, only on existing records, which is, well,
# idiotic.
values = self._do_validate(entity.to_dict())
try:
LOG.debug("Saving entity...")
entity.save(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem saving entity for create')
if values:
values_id = values['id']
else:
values_id = None
raise exception.Duplicate("Entity ID {0} already exists!"
.format(values_id))
try:
LOG.debug("Saving entity...")
entity.save(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem saving entity for create')
if values:
values_id = values['id']
else:
values_id = None
raise exception.Duplicate("Entity ID {0} already exists!"
.format(values_id))
LOG.debug('Elapsed repo '
'create secret:%s', (time.time() - start)) # DEBUG
@@ -373,23 +412,21 @@ class BaseRepo(object):
:raises NotFound if entity does not exist.
"""
session = get_session()
with session.begin():
entity.updated_at = timeutils.utcnow()
entity.updated_at = timeutils.utcnow()
# Validate the attributes before we go any further. From my
# (unknown Glance developer) investigation, the @validates
# decorator does not validate
# on new records, only on existing records, which is, well,
# idiotic.
self._do_validate(entity.to_dict())
# Validate the attributes before we go any further. From my
# (unknown Glance developer) investigation, the @validates
# decorator does not validate
# on new records, only on existing records, which is, well,
# idiotic.
self._do_validate(entity.to_dict())
try:
entity.save(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem saving entity for update')
raise exception.NotFound("Entity ID %s not found"
% entity.id)
try:
entity.save()
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem saving entity for update')
raise exception.NotFound("Entity ID %s not found"
% entity.id)
def update(self, entity_id, values, purge_props=False):
"""Set the given properties on an entity and update it.
@@ -402,17 +439,16 @@ class BaseRepo(object):
"""Remove the entity by its ID."""
session = get_session()
with session.begin():
entity = self.get(entity_id=entity_id, keystone_id=keystone_id,
session=session)
entity = self.get(entity_id=entity_id, keystone_id=keystone_id,
session=session)
try:
entity.delete(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem finding entity to delete')
raise exception.NotFound("Entity ID %s not found"
% entity_id)
try:
entity.delete(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem finding entity to delete')
raise exception.NotFound("Entity ID %s not found"
% entity_id)
def _do_entity_name(self):
"""Sub-class hook: return entity name, such as for debugging."""
@@ -462,39 +498,38 @@ class BaseRepo(object):
find and update it
"""
session = get_session()
with session.begin():
if entity_id:
entity_ref = self.get(entity_id, session=session)
values['updated_at'] = timeutils.utcnow()
else:
self._do_convert_values(values)
entity_ref = self._do_create_instance()
# Need to canonicalize ownership
if 'owner' in values and not values['owner']:
values['owner'] = None
entity_ref.update(values)
# Validate the attributes before we go any further. From my
# (unknown Glance developer) investigation, the @validates
# decorator does not validate
# on new records, only on existing records, which is, well,
# idiotic.
self._do_validate(entity_ref.to_dict())
self._update_values(entity_ref, values)
try:
entity_ref.save(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem saving entity for _update')
if entity_id:
entity_ref = self.get(entity_id, session=session)
values['updated_at'] = timeutils.utcnow()
raise exception.NotFound("Entity ID %s not found"
% entity_id)
else:
self._do_convert_values(values)
entity_ref = self._do_create_instance()
# Need to canonicalize ownership
if 'owner' in values and not values['owner']:
values['owner'] = None
entity_ref.update(values)
# Validate the attributes before we go any further. From my
# (unknown Glance developer) investigation, the @validates
# decorator does not validate
# on new records, only on existing records, which is, well,
# idiotic.
self._do_validate(entity_ref.to_dict())
self._update_values(entity_ref, values)
try:
entity_ref.save(session=session)
except sqlalchemy.exc.IntegrityError:
LOG.exception('Problem saving entity for _update')
if entity_id:
raise exception.NotFound("Entity ID %s not found"
% entity_id)
else:
raise exception.Duplicate("Entity ID %s already exists!"
% values['id'])
raise exception.Duplicate("Entity ID %s already exists!"
% values['id'])
return self.get(entity_ref.id)
@@ -664,30 +699,29 @@ class SecretStoreMetadatumRepo(BaseRepo):
:raises NotFound if entity does not exist.
"""
now = timeutils.utcnow()
session = get_session()
with session.begin():
for k, v in metadata.items():
meta_model = models.SecretStoreMetadatum(k, v)
meta_model.updated_at = now
meta_model.secret = secret_model
meta_model.save(session=session)
for k, v in metadata.items():
meta_model = models.SecretStoreMetadatum(k, v)
meta_model.updated_at = now
meta_model.secret = secret_model
meta_model.save()
def get_metadata_for_secret(self, secret_id):
"""Returns a dict of SecretStoreMetadatum instances."""
session = get_session()
with session.begin():
try:
query = session.query(models.SecretStoreMetadatum)
query = query.filter_by(deleted=False)
query = query.filter(
models.SecretStoreMetadatum.secret_id == secret_id)
try:
query = session.query(models.SecretStoreMetadatum)
query = query.filter_by(deleted=False)
metadata = query.all()
query = query.filter(
models.SecretStoreMetadatum.secret_id == secret_id)
except sa_orm.exc.NoResultFound:
metadata = dict()
metadata = query.all()
except sa_orm.exc.NoResultFound:
metadata = dict()
return dict((m.key, m.value) for m in metadata)
@@ -871,29 +905,29 @@ class OrderPluginMetadatumRepo(BaseRepo):
"""
now = timeutils.utcnow()
session = get_session()
with session.begin():
for k, v in metadata.items():
meta_model = models.OrderPluginMetadatum(k, v)
meta_model.updated_at = now
meta_model.order = order_model
meta_model.save(session=session)
for k, v in metadata.items():
meta_model = models.OrderPluginMetadatum(k, v)
meta_model.updated_at = now
meta_model.order = order_model
meta_model.save(session=session)
def get_metadata_for_order(self, order_id):
"""Returns a dict of OrderPluginMetadatum instances."""
session = get_session()
with session.begin():
try:
query = session.query(models.OrderPluginMetadatum)
query = query.filter_by(deleted=False)
query = query.filter(
models.OrderPluginMetadatum.order_id == order_id)
try:
query = session.query(models.OrderPluginMetadatum)
query = query.filter_by(deleted=False)
metadata = query.all()
query = query.filter(
models.OrderPluginMetadatum.order_id == order_id)
except sa_orm.exc.NoResultFound:
metadata = dict()
metadata = query.all()
except sa_orm.exc.NoResultFound:
metadata = dict()
return dict((m.key, m.value) for m in metadata)
@@ -1064,11 +1098,12 @@ class ContainerConsumerRepo(BaseRepo):
def create_from(self, new_consumer, container):
session = get_session()
try:
with session.begin():
container.updated_at = timeutils.utcnow()
container.consumers.append(new_consumer)
container.save(session=session)
container.updated_at = timeutils.utcnow()
container.consumers.append(new_consumer)
container.save(session=session)
except sqlalchemy.exc.IntegrityError:
session.rollback() # We know consumer already exists.
# This operation is idempotent, so log this and move on
LOG.debug("Consumer %s already exists for container %s,"
" continuing...", (new_consumer.name, new_consumer.URL),
+1
View File
@@ -296,6 +296,7 @@ def _find_or_create_kek_objects(plugin_inst, tenant_model):
def _store_secret_and_datum(
context, secret_model, kek_datum_model, generated_dto):
# Create Secret entities in data store.
if not secret_model.id:
repositories.get_secret_repository().create_from(secret_model)
+8 -2
View File
@@ -49,6 +49,7 @@ CONF.register_opts(queue_opts, group=queue_opt_group)
TRANSPORT = None
IS_SERVER_SIDE = True
ALLOWED_EXMODS = [
exception.__name__,
@@ -59,13 +60,18 @@ def get_allowed_exmods():
return ALLOWED_EXMODS
def init(conf):
global TRANSPORT
def init(conf, is_server_side=True):
global TRANSPORT, IS_SERVER_SIDE
exmods = get_allowed_exmods()
IS_SERVER_SIDE = is_server_side
TRANSPORT = messaging.get_transport(conf,
allowed_remote_exmods=exmods)
def is_server_side():
return IS_SERVER_SIDE
def cleanup():
global TRANSPORT
assert TRANSPORT is not None
+34
View File
@@ -16,9 +16,12 @@
"""
Server-side (i.e. worker side) classes and logic.
"""
import functools
from oslo.config import cfg
from barbican.common import utils
from barbican.model import repositories
from barbican.openstack.common import service
from barbican import queue
from barbican.tasks import resources
@@ -29,6 +32,35 @@ LOG = utils.getLogger(__name__)
CONF = cfg.CONF
def transactional(fn):
"""Provides request-scoped database transaction support to tasks."""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
if not queue.is_server_side():
fn(*args, **kwargs) # Non-server mode directly invokes tasks.
else:
# Start the database session.
repositories.start()
# Manage session/transaction.
try:
fn(*args, **kwargs)
repositories.commit()
except Exception:
"""NOTE: Wrapped functions must process with care!
Exceptions that reach here will revert the entire transaction,
including any updates made to entities such as setting error
codes and error messages.
"""
repositories.rollback()
finally:
repositories.clear()
return wrapper
class Tasks(object):
"""Tasks that can be invoked asynchronously in Barbican.
@@ -41,6 +73,7 @@ class Tasks(object):
methods on itself, which include the methods in this class.
"""
@transactional
def process_type_order(self, context, order_id, keystone_id):
"""Process TypeOrder."""
LOG.debug('TypeOrder id is {0}'.format(order_id))
@@ -51,6 +84,7 @@ class Tasks(object):
LOG.exception(">>>>> Task exception seen, details reported "
"on the Orders entity.")
@transactional
def update_order(self, context, order_id, keystone_id, updated_meta):
"""Update Order."""
task = resources.UpdateOrder()
+111
View File
@@ -19,20 +19,129 @@ from barbican.queue import server
from barbican.tests import utils
class WhenUsingTransactionalDecorator(utils.BaseTestCase):
"""Test using the 'transactional' decorator in server.py.
Note that only the 'I am a server' logic is tested here, as the alternate
mode is only used for direct invocation of Task methods in the standalone
server mode, which is also thoroughly tested in WhenUsingBeginTypeOrderTask
below.
"""
def setUp(self):
super(WhenUsingTransactionalDecorator, self).setUp()
# Ensure we always thing we are in 'I am a server' mode.
is_server_side_config = {
'return_value': True
}
self.is_server_side_patcher = mock.patch(
'barbican.queue.is_server_side',
**is_server_side_config
)
self.is_server_side_patcher.start()
# Mock the various repository calls:
self.start_patcher = mock.patch(
'barbican.model.repositories.start'
)
self.start_mock = self.start_patcher.start()
self.commit_patcher = mock.patch(
'barbican.model.repositories.commit'
)
self.commit_mock = self.commit_patcher.start()
self.rollback_patcher = mock.patch(
'barbican.model.repositories.rollback'
)
self.rollback_mock = self.rollback_patcher.start()
self.clear_patcher = mock.patch(
'barbican.model.repositories.clear'
)
self.clear_mock = self.clear_patcher.start()
self.args = ('foo', 'bar')
self.kwargs = {'k_foo': 1, 'k_bar': 2}
# Class/decorator under test.
class TestClass(object):
my_args = None
my_kwargs = None
is_exception_needed = False
@server.transactional
def test_method(self, *args, **kwargs):
if self.is_exception_needed:
raise ValueError()
self.my_args = args
self.my_kwargs = kwargs
self.test_object = TestClass()
def tearDown(self):
super(WhenUsingTransactionalDecorator, self).tearDown()
self.is_server_side_patcher.stop()
self.start_patcher.stop()
self.commit_patcher.stop()
self.rollback_patcher.stop()
self.clear_patcher.stop()
def test_should_commit(self):
self.test_object.test_method(*self.args, **self.kwargs)
self.assertEqual(self.args, self.test_object.my_args)
self.assertEqual(self.kwargs, self.test_object.my_kwargs)
self.assertEqual(self.start_mock.call_count, 1)
self.assertEqual(self.commit_mock.call_count, 1)
self.assertEqual(self.rollback_mock.call_count, 0)
self.assertEqual(self.clear_mock.call_count, 1)
def test_should_rollback(self):
self.test_object.is_exception_needed = True
self.test_object.test_method(*self.args, **self.kwargs)
self.assertEqual(self.start_mock.call_count, 1)
self.assertEqual(self.commit_mock.call_count, 0)
self.assertEqual(self.rollback_mock.call_count, 1)
self.assertEqual(self.clear_mock.call_count, 1)
class WhenUsingBeginTypeOrderTask(utils.BaseTestCase):
"""Test using the Tasks class for 'type order' task."""
def setUp(self):
super(WhenUsingBeginTypeOrderTask, self).setUp()
# Mock the 'am I a server process?' flag used by the decorator around
# all task methods. Since this test class focuses on testing task
# method behaviors, this flag is set to false to allow for direct
# testing of these tasks without database transactional interference.
is_server_side_config = {
'return_value': False
}
self.is_server_side_patcher = mock.patch(
'barbican.queue.is_server_side',
**is_server_side_config
)
self.is_server_side_patcher.start()
self.tasks = server.Tasks()
def tearDown(self):
super(WhenUsingBeginTypeOrderTask, self).tearDown()
self.is_server_side_patcher.stop()
@mock.patch('barbican.tasks.resources.BeginTypeOrder')
def test_should_process_order(self, mock_begin_order):
mock_begin_order.return_value.process.return_value = None
self.tasks.process_type_order(context=None,
order_id=self.order_id,
keystone_id=self.keystone_id)
mock_begin_order.return_value.process.assert_called_with(
self.order_id, self.keystone_id)
@@ -40,6 +149,7 @@ class WhenUsingBeginTypeOrderTask(utils.BaseTestCase):
def test_should_update_order(self, mock_update_order):
mock_update_order.return_value.process.return_value = None
updated_meta = {}
self.tasks.update_order(context=None,
order_id=self.order_id,
keystone_id=self.keystone_id,
@@ -52,6 +162,7 @@ class WhenUsingBeginTypeOrderTask(utils.BaseTestCase):
def test_process_order_catch_exception(self, mock_begin_order):
"""Test process_type_order() handles all exceptions."""
mock_begin_order.return_value.process.side_effect = Exception()
self.tasks.process_type_order(None, self.order_id, self.keystone_id)