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
|
import click
|
||||||
|
|
||||||
from solar.dblayer.model import DBLayerException
|
from solar.dblayer.model import DBLayerException
|
||||||
|
from solar.dblayer.utils import Atomic
|
||||||
from solar.errors import SolarError
|
from solar.errors import SolarError
|
||||||
|
|
||||||
|
|
||||||
@ -45,10 +46,19 @@ class AliasedGroup(click.Group):
|
|||||||
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))
|
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):
|
class BaseGroup(click.Group):
|
||||||
|
|
||||||
error_wrapper_enabled = False
|
error_wrapper_enabled = False
|
||||||
|
|
||||||
def add_command(self, cmd, name=None):
|
def add_command(self, cmd, name=None):
|
||||||
|
|
||||||
cmd.callback = self.error_wrapper(cmd.callback)
|
cmd.callback = self.error_wrapper(cmd.callback)
|
||||||
return super(BaseGroup, self).add_command(cmd, name)
|
return super(BaseGroup, self).add_command(cmd, name)
|
||||||
|
|
||||||
@ -69,3 +79,7 @@ class BaseGroup(click.Group):
|
|||||||
|
|
||||||
def handle_exception(self, e):
|
def handle_exception(self, e):
|
||||||
pass
|
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:
|
else:
|
||||||
r = create_resource(name, spec, inputs=inputs, tags=tags)
|
r = create_resource(name, spec, inputs=inputs, tags=tags)
|
||||||
rs = [r]
|
rs = [r]
|
||||||
|
|
||||||
return CreatedResources(rs)
|
return CreatedResources(rs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -616,6 +616,13 @@ class ModelMeta(type):
|
|||||||
continue
|
continue
|
||||||
cls._c.lazy_save.clear()
|
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
|
@classmethod
|
||||||
def session_end(mcs, result=True):
|
def session_end(mcs, result=True):
|
||||||
mcs.save_all_lazy()
|
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