Merge "Fix: Transaction rollback following DB creation error"

This commit is contained in:
Zuul 2018-09-12 17:03:34 +00:00 committed by Gerrit Code Review
commit 85896437af
3 changed files with 89 additions and 50 deletions

View File

@ -62,7 +62,11 @@ class BucketsResource(api_base.BaseResource):
documents = self._encrypt_secret_documents(documents) documents = self._encrypt_secret_documents(documents)
created_documents = self._create_revision_documents( created_documents = self._create_revision_documents(
bucket_name, documents, validations) bucket_name, documents)
if created_documents:
revision_id = created_documents[0]['revision_id']
self._create_revision_validations(revision_id, validations)
resp.body = self.view_builder.list(created_documents) resp.body = self.view_builder.list(created_documents)
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
@ -75,14 +79,17 @@ class BucketsResource(api_base.BaseResource):
document['data'] = secret_ref document['data'] = secret_ref
return documents return documents
def _create_revision_documents(self, bucket_name, documents, def _create_revision_documents(self, bucket_name, documents):
validations):
try: try:
created_documents = db_api.documents_create( created_documents = db_api.documents_create(bucket_name, documents)
bucket_name, documents, validations=validations)
except (deckhand_errors.DuplicateDocumentExists, except (deckhand_errors.DuplicateDocumentExists,
deckhand_errors.SingletonDocumentConflict) as e: deckhand_errors.SingletonDocumentConflict) as e:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.exception(e.format_message()) LOG.exception(e.format_message())
return created_documents return created_documents
def _create_revision_validations(self, revision_id, validations):
for validation in validations:
db_api.validation_create(revision_id, validation['name'],
validation)

View File

@ -149,8 +149,7 @@ def require_unique_document_schema(schema=None):
@require_unique_document_schema(types.LAYERING_POLICY_SCHEMA) @require_unique_document_schema(types.LAYERING_POLICY_SCHEMA)
def documents_create(bucket_name, documents, validations=None, def documents_create(bucket_name, documents, session=None):
session=None):
"""Create a set of documents and associated bucket. """Create a set of documents and associated bucket.
If no changes are detected, a new revision will not be created. This If no changes are detected, a new revision will not be created. This
@ -160,36 +159,35 @@ def documents_create(bucket_name, documents, validations=None,
:param bucket_name: The name of the bucket with which to associate created :param bucket_name: The name of the bucket with which to associate created
documents. documents.
:param documents: List of documents to be created. :param documents: List of documents to be created.
:param validation_policies: List of validation policies to be created.
:param session: Database session object. :param session: Database session object.
:returns: List of created documents in dictionary format. :returns: List of created documents in dictionary format.
:raises DocumentExists: If the document already exists in the DB for any :raises DocumentExists: If the document already exists in the DB for any
bucket. bucket.
""" """
session = session or get_session() session = session or get_session()
documents_to_create = _documents_create(bucket_name, documents, session)
resp = [] resp = []
# The documents to be deleted are computed by comparing the documents for with session.begin():
# the previous revision (if it exists) that belong to `bucket_name` with documents_to_create = _documents_create(bucket_name, documents,
# `documents`: the difference between the former and the latter. session=session)
# The documents to be deleted are computed by comparing the documents
# for the previous revision (if it exists) that belong to `bucket_name`
# with `documents`: the difference between the former and the latter.
document_history = [ document_history = [
d for d in revision_documents_get(bucket_name=bucket_name) d for d in revision_documents_get(bucket_name=bucket_name,
session=session)
] ]
documents_to_delete = [ documents_to_delete = [
h for h in document_history if _meta(h) not in [ h for h in document_history if _meta(h) not in [
_meta(d) for d in documents] _meta(d) for d in documents]
] ]
# Only create a revision if any docs have been created, changed or deleted. # Only create a revision if any docs have been created, changed or
# deleted.
if any([documents_to_create, documents_to_delete]): if any([documents_to_create, documents_to_delete]):
bucket = bucket_get_or_create(bucket_name) revision = revision_create(session=session)
revision = revision_create() bucket = bucket_get_or_create(bucket_name, session=session)
if validations:
for validation in validations:
validation_create(revision['id'], validation['name'],
validation)
if documents_to_delete: if documents_to_delete:
LOG.debug('Deleting documents: %s.', LOG.debug('Deleting documents: %s.',
@ -198,7 +196,6 @@ def documents_create(bucket_name, documents, validations=None,
for d in documents_to_delete: for d in documents_to_delete:
doc = models.Document() doc = models.Document()
with session.begin():
# Store bare minimum information about the document. # Store bare minimum information about the document.
doc['schema'] = d['schema'] doc['schema'] = d['schema']
doc['name'] = d['name'] doc['name'] = d['name']
@ -223,10 +220,12 @@ def documents_create(bucket_name, documents, validations=None,
if documents_to_create: if documents_to_create:
LOG.debug( LOG.debug(
'Creating documents: %s.', [(d['schema'], d['layer'], d['name']) 'Creating documents: %s.', [
for d in documents_to_create]) (d['schema'], d['layer'], d['name'])
for d in documents_to_create
]
)
for doc in documents_to_create: for doc in documents_to_create:
with session.begin():
doc['bucket_id'] = bucket['id'] doc['bucket_id'] = bucket['id']
doc['revision_id'] = revision['id'] doc['revision_id'] = revision['id']
if not doc.get('orig_revision_id'): if not doc.get('orig_revision_id'):
@ -256,7 +255,6 @@ def _documents_create(bucket_name, documents, session=None):
def _document_create(document): def _document_create(document):
model = models.Document() model = models.Document()
with session.begin():
model.update(document) model.update(document)
return model return model
@ -457,7 +455,6 @@ def bucket_get_or_create(bucket_name, session=None):
.one() .one()
except sa_orm.exc.NoResultFound: except sa_orm.exc.NoResultFound:
bucket = models.Bucket() bucket = models.Bucket()
with session.begin():
bucket.update({'name': bucket_name}) bucket.update({'name': bucket_name})
bucket.save(session=session) bucket.save(session=session)
@ -476,7 +473,6 @@ def revision_create(session=None):
session = session or get_session() session = session or get_session()
revision = models.Revision() revision = models.Revision()
with session.begin():
revision.save(session=session) revision.save(session=session)
return revision.to_dict() return revision.to_dict()

View File

@ -12,6 +12,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import mock
import testtools
from deckhand.db.sqlalchemy import api as db_api
from deckhand import errors
from deckhand import factories from deckhand import factories
from deckhand.tests import test_utils from deckhand.tests import test_utils
from deckhand.tests.unit.db import base from deckhand.tests.unit.db import base
@ -299,3 +304,34 @@ class TestDocuments(base.TestDbBase):
sorted(orig_documents, key=lambda d: d['created_at']), sorted(orig_documents, key=lambda d: d['created_at']),
sorted(duplicate_documents, key=lambda d: d['created_at']), sorted(duplicate_documents, key=lambda d: d['created_at']),
ignore=['created_at', 'updated_at', 'revision_id', 'id']) ignore=['created_at', 'updated_at', 'revision_id', 'id'])
def test_document_creation_failure_rolls_back_in_flight_revision(self):
"""Regression test that an exception that occurs between creation of
a revision and creation of all bucket documents results in the
in-flight database objects getting rolled back.
"""
bucket_name = test_utils.rand_name('bucket')
payload = base.DocumentFixture.get_minimal_fixture()
original_revision_create = db_api.revision_create
# Mock the revision_create function so we can assert whether it was
# called, but still call the real function.
with mock.patch.object(
db_api, 'revision_create', autospec=True,
side_effect=original_revision_create) as m_revision_create:
# Raise any exception on a function call following the creation of
# a revision. The transaction will not complete so the result will
# not be committed to the database.
with mock.patch.object(db_api, 'bucket_get_or_create',
autospec=True,
side_effect=errors.DuplicateDocumentExists):
with testtools.ExpectedException(
errors.DuplicateDocumentExists):
self.create_documents(bucket_name, [payload])
# Validate that the actual revision_create call was indeed invoked.
self.assertTrue(m_revision_create.called)
# Finally validate that the revision doesn't exist.
with testtools.ExpectedException(errors.RevisionNotFound):
self.show_revision(1)