Add helper to preserve atomicity for database group operations
All solar cli actions will be atomic, for example if user is going to create resources from composer file - all or none will be added to database If for some reason this behaviour is undesirable for particular command developer can overwrite it by using default click command: @group.command(cls=click.Command) For those who are using solar as a library - decorator and context managers are available in following module: from solar.dblayer.utils import atomic @atomic def setup(): Change-Id: I8491d90f17c25edc85f18bc7bd7e16c32c3f4561
This commit is contained in:
parent
fb86cb1b1d
commit
7edc781d66
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
81
solar/dblayer/test/test_atomic.py
Normal file
81
solar/dblayer/test/test_atomic.py
Normal 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
45
solar/dblayer/utils.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user