deb-glare/glare/locking.py

137 lines
4.7 KiB
Python

# Copyright 2016 OpenStack Foundation
# All Rights Reserved.
#
# 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 inspect
import six
from oslo_log import log as logging
from glare.i18n import _LI
LOG = logging.getLogger(__name__)
class LockApiBase(object):
"""Lock Api Base class that responsible for acquiring/releasing locks
"""
def create_lock(self, context, lock_key):
"""Acquire lock for current user
:param context user context
:param lock_key: unique lock identifier that defines lock scope
:return lock internal identifier
"""
raise NotImplementedError()
def delete_lock(self, context, lock_id):
"""Delete acquired user lock
:param context: user context
:param lock_id: lock internal identifier
:return:
"""
raise NotImplementedError()
class Lock(object):
"""Object that stores lock context for users. This class is internal
and used only for Lock Engine. So users shouldn't use this class directly
"""
def __init__(self, context, lock_id, lock_key, release_method):
"""Initialize lock context"""
self.context = context
self.lock_id = lock_id
self.lock_key = lock_key
self.release = release_method
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# TODO(kairat) catch all exceptions here
self.release(self)
class LockEngine(object):
"""Glare lock engine.
Defines how artifact updates must be synchronized with each other. When
some user obtains lock for the same piece of data then other user cannot
request that lock and get Conflict error.
This little engine also allows to encapsulate lock logic in one place so
we can potentially add tooz functionality in future to Glare. Right now
there are troubles with locks in Galera (especially in mysql) and zookeeper
requires additional work from IT engineers. So we need support production
ready DB locks in our implementation.
"""
MAX_LOCK_LENGTH = 255
def __init__(self, lock_api):
"""Initialize lock engine with some lock api
:param lock_api: api that allows to create/delete locks. It must be
db_api but it might be replaced with DLM in near future.
"""
self.lock_api = lock_api
def acquire(self, context, lock_key):
"""Acquire lock to update whole artifact
Acquire lock to update artifact. If there is some other
lock for the same artifact then raise Conflict Error.
:param context: user context
:param lock_key: lock key
:return: lock definition
"""
if lock_key is not None and len(lock_key) < self.MAX_LOCK_LENGTH:
lock_id = self.lock_api.create_lock(context, lock_key)
LOG.info(_LI("Lock %(lock_id)s acquired for lock_key "
"%(lock_key)s"),
{'lock_id': lock_id, 'lock_key': lock_key})
else:
lock_id = None
LOG.info(_LI("No lock for lock_key %s"), lock_key)
return Lock(context, lock_id, lock_key, self.release)
def release(self, lock):
if lock.lock_id is not None:
self.lock_api.delete_lock(lock.context, lock.lock_id)
LOG.info(_LI("Lock %(lock_id)s released for lock_key %(key)s"),
{'lock_id': lock.lock_id, 'key': lock.lock_key})
def locked(self, lock_name_parameters):
"""Synchronization decorator.
:param list lock_name_parameters: List of parameters that will be used
as part of lock name
:returns: function that locks artifact by specified parameters
"""
def wrap(f):
@six.wraps(f)
def wrapped(*a, **k):
call_args = inspect.getcallargs(f, *a, **k)
lock_key = ''
for param in lock_name_parameters:
lock_key += str(call_args[param]) + ':'
context = call_args.get('context')
with self.acquire(context, lock_key):
res = f(*a, **k)
return res
return wrapped
return wrap