Add a generic coordination lock mechanism

For various synchronized scenarios, this decorator
allows flexible lock name with parameters and names
of underlying functions.
For instance:
    @synchronized('{f_name}-{resource.id}-{snap[name]}')
    def foo(self, resource, snap):

Change-Id: I4bf75be2902cd598a5a5a2c5887d4b4262f3e042
Related-Bug: #1824911
This commit is contained in:
LIU Yulong 2019-06-02 20:05:27 +08:00
parent 6002df86fb
commit 975143fd08
4 changed files with 140 additions and 0 deletions

View File

@ -0,0 +1,96 @@
#
# 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.
"""Coordination and locking utilities."""
import inspect
import decorator
from oslo_concurrency import lockutils
from oslo_log import log
from oslo_utils import timeutils
import six
LOG = log.getLogger(__name__)
def synchronized(lock_name):
"""Synchronization decorator.
:param str lock_name: Lock name.
Decorating a method like so::
@synchronized('mylock')
def foo(self, *args):
...
ensures that only one process will execute the foo method at a time.
Different methods can share the same lock::
@synchronized('mylock')
def foo(self, *args):
...
@synchronized('mylock')
def bar(self, *args):
...
This way only one of either foo or bar can be executing at a time.
Lock name can be formatted using Python format string syntax::
@synchronized('{f_name}-{resource.id}-{snap[name]}')
def foo(self, resource, snap):
...
Available field names are: decorated function parameters and
`f_name` as a decorated function name.
"""
@decorator.decorator
def _synchronized(f, *a, **k):
if six.PY2:
# pylint: disable=deprecated-method
call_args = inspect.getcallargs(f, *a, **k)
else:
sig = inspect.signature(f).bind(*a, **k)
sig.apply_defaults()
call_args = sig.arguments
call_args['f_name'] = f.__name__
lock_format_name = lock_name.format(**call_args)
t1 = timeutils.now()
t2 = None
try:
with lockutils.lock(lock_format_name):
t2 = timeutils.now()
LOG.debug('Lock "%(name)s" acquired by "%(function)s" :: '
'waited %(wait_secs)0.3fs',
{'name': lock_format_name,
'function': f.__name__,
'wait_secs': (t2 - t1)})
return f(*a, **k)
finally:
t3 = timeutils.now()
if t2 is None:
held_secs = "N/A"
else:
held_secs = "%0.3fs" % (t3 - t2)
LOG.debug('Lock "%(name)s" released by "%(function)s" :: held '
'%(held_secs)s',
{'name': lock_format_name,
'function': f.__name__,
'held_secs': held_secs})
return _synchronized

View File

@ -0,0 +1,33 @@
#
# 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 mock
from oslo_concurrency import lockutils
from neutron.common import coordination
from neutron.tests import base
@mock.patch.object(lockutils, 'lock')
class CoordinationTestCase(base.BaseTestCase):
def test_synchronized(self, get_lock):
@coordination.synchronized('lock-{f_name}-{foo.val}-{bar[val]}')
def func(foo, bar):
pass
foo = mock.Mock()
foo.val = 7
bar = mock.MagicMock()
bar.__getitem__.return_value = 8
func(foo, bar)
get_lock.assert_called_with('lock-func-7-8')

View File

@ -0,0 +1,10 @@
---
other:
- |
Add a generic coordination lock mechanism for various
scenarios. This decorator allows flexible lock name
with parameters and names of underlying functions. And
in order to achive backward compatibility with python2.7
several functions was copied from the old version of
python inspect. Once python2.7 is retired, we can drop
such duplication.

View File

@ -7,6 +7,7 @@ Paste>=2.0.2 # MIT
PasteDeploy>=1.5.0 # MIT
Routes>=2.3.1 # MIT
debtcollector>=1.2.0 # Apache-2.0
decorator>=3.4.0 # BSD
eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT
pecan>=1.3.2 # BSD
httplib2>=0.9.1 # MIT