Merge "Add helper to preserve atomicity for database group operations"

This commit is contained in:
Jenkins 2016-05-05 23:39:03 +00:00 committed by Gerrit Code Review
commit 0b50415257
5 changed files with 147 additions and 1 deletions

View File

@ -17,6 +17,7 @@ from functools import wraps
import click
from solar.dblayer.model import DBLayerException
from solar.dblayer.utils import Atomic
from solar.errors import SolarError
@ -45,10 +46,19 @@ class AliasedGroup(click.Group):
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
class AtomicCommand(click.Command):
def invoke(self, *args, **kwargs):
with Atomic():
return super(AtomicCommand, self).invoke(*args, **kwargs)
class BaseGroup(click.Group):
error_wrapper_enabled = False
def add_command(self, cmd, name=None):
cmd.callback = self.error_wrapper(cmd.callback)
return super(BaseGroup, self).add_command(cmd, name)
@ -69,3 +79,7 @@ class BaseGroup(click.Group):
def handle_exception(self, e):
pass
def command(self, *args, **kwargs):
kwargs.setdefault('cls', AtomicCommand)
return super(BaseGroup, self).command(*args, **kwargs)

View File

@ -100,7 +100,6 @@ def create(name, spec, inputs=None, tags=None):
else:
r = create_resource(name, spec, inputs=inputs, tags=tags)
rs = [r]
return CreatedResources(rs)

View File

@ -616,6 +616,13 @@ class ModelMeta(type):
continue
cls._c.lazy_save.clear()
@classmethod
def find_non_empty_lazy_saved(mcs):
for cls in mcs._defined_models:
if cls._c.lazy_save:
return cls._c.lazy_save
return None
@classmethod
def session_end(mcs, result=True):
mcs.save_all_lazy()

View File

@ -0,0 +1,81 @@
# Copyright 2016 Mirantis, Inc.
#
# 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 pytest
from solar.dblayer.model import DBLayerException
from solar.dblayer.model import DBLayerNotFound
from solar.dblayer.model import Field
from solar.dblayer.model import Model
from solar.dblayer.model import ModelMeta
from solar.dblayer import utils
class T1(Model):
fi1 = Field(str)
def save_multiple(key1, key2):
ex1 = T1.from_dict({'key': key1, 'fi1': 'blah blah live'})
ex1.save_lazy()
ex2 = T1.from_dict({'key': key2, 'fi1': 'blah blah another live'})
ex2.save_lazy()
atomic_save_multiple = utils.atomic(save_multiple)
def test_one_will_be_saved(rk):
key = next(rk)
with pytest.raises(DBLayerException):
save_multiple(key, key)
ModelMeta.session_end()
assert T1.get(key)
def test_atomic_none_saved(rk):
key = next(rk)
with pytest.raises(DBLayerException):
with utils.Atomic():
save_multiple(key, key)
with pytest.raises(DBLayerNotFound):
assert T1.get(key)
def test_atomic_decorator_none_saved(rk):
key = next(rk)
with pytest.raises(DBLayerException):
atomic_save_multiple(key, key)
with pytest.raises(DBLayerNotFound):
assert T1.get(key)
def test_atomic_save_all(rk):
key1, key2 = (next(rk) for _ in range(2))
atomic_save_multiple(key1, key2)
assert T1.get(key1)
assert T1.get(key2)
def test_atomic_helper_validation(rk):
key1, key2, key3 = (next(rk) for _ in range(3))
ex1 = T1.from_dict({'key': key1, 'fi1': 'stuff'})
ex1.save_lazy()
with pytest.raises(DBLayerException):
atomic_save_multiple(key1, key2)

45
solar/dblayer/utils.py Normal file
View File

@ -0,0 +1,45 @@
# Copyright 2016 Mirantis, Inc.
#
# 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 wrapt
from solar.dblayer.model import DBLayerException
from solar.dblayer import ModelMeta
class Atomic(object):
def __enter__(self):
lazy_saved = ModelMeta.find_non_empty_lazy_saved()
if lazy_saved:
raise DBLayerException(
'Some objects could be accidentally rolled back on failure, '
'Please ensure that atomic helper is initiated '
'before any object is saved. '
'See list of objects: %r', lazy_saved)
ModelMeta.session_start()
def __exit__(self, *exc_info):
# if there was an exception - rollback immediatly,
# else catch any during save - and rollback in case of failure
try:
ModelMeta.session_end(result=not any(exc_info))
except Exception:
ModelMeta.session_end(result=False)
@wrapt.decorator
def atomic(wrapped, instance, args, kwargs):
with Atomic():
return wrapped(*args, **kwargs)