From 975143fd08f38819859b2251da568af58c8f3a33 Mon Sep 17 00:00:00 2001 From: LIU Yulong Date: Sun, 2 Jun 2019 20:05:27 +0800 Subject: [PATCH] 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 --- neutron/common/coordination.py | 96 +++++++++++++++++++ .../tests/unit/common/test_coordination.py | 33 +++++++ .../notes/coordination-df3c0bf55a0c4863.yaml | 10 ++ requirements.txt | 1 + 4 files changed, 140 insertions(+) create mode 100644 neutron/common/coordination.py create mode 100644 neutron/tests/unit/common/test_coordination.py create mode 100644 releasenotes/notes/coordination-df3c0bf55a0c4863.yaml diff --git a/neutron/common/coordination.py b/neutron/common/coordination.py new file mode 100644 index 00000000000..ffe065aab9f --- /dev/null +++ b/neutron/common/coordination.py @@ -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 diff --git a/neutron/tests/unit/common/test_coordination.py b/neutron/tests/unit/common/test_coordination.py new file mode 100644 index 00000000000..a8aeae6752f --- /dev/null +++ b/neutron/tests/unit/common/test_coordination.py @@ -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') diff --git a/releasenotes/notes/coordination-df3c0bf55a0c4863.yaml b/releasenotes/notes/coordination-df3c0bf55a0c4863.yaml new file mode 100644 index 00000000000..6c6d8614442 --- /dev/null +++ b/releasenotes/notes/coordination-df3c0bf55a0c4863.yaml @@ -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. diff --git a/requirements.txt b/requirements.txt index b817cabef26..83f21c41fb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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