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:
Dmitry Shulyak 2016-05-04 13:47:30 +03:00
parent fb86cb1b1d
commit 7edc781d66
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)