Coverage for manila/coordination.py: 93%
66 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
13"""Tooz Coordination and locking utilities."""
15import inspect
17import decorator
18from oslo_config import cfg
19from oslo_log import log
20from oslo_utils import uuidutils
21from tooz import coordination
22from tooz import locking
24from manila import exception
25from manila.i18n import _
28LOG = log.getLogger(__name__)
31coordination_opts = [
32 cfg.StrOpt('backend_url',
33 secret=True,
34 default='file://$state_path',
35 help='The back end URL to use for distributed coordination.')
36]
38CONF = cfg.CONF
39CONF.register_opts(coordination_opts, group='coordination')
42class Coordinator(object):
43 """Tooz coordination wrapper.
45 Coordination member id is created from concatenated `prefix` and
46 `agent_id` parameters.
48 :param str agent_id: Agent identifier
49 :param str prefix: Used to provide member identifier with a
50 meaningful prefix.
51 """
53 def __init__(self, agent_id=None, prefix=''):
54 self.coordinator = None
55 self.agent_id = agent_id or uuidutils.generate_uuid()
56 self.started = False
57 self.prefix = prefix
59 def start(self):
60 """Connect to coordination back end."""
61 if self.started:
62 return
64 # NOTE(gouthamr): Tooz expects member_id as a byte string.
65 member_id = (self.prefix + self.agent_id).encode('ascii')
66 self.coordinator = coordination.get_coordinator(
67 cfg.CONF.coordination.backend_url, member_id)
68 self.coordinator.start(start_heart=True)
69 self.started = True
71 def stop(self):
72 """Disconnect from coordination back end."""
73 msg = 'Stopped Coordinator (Agent ID: %(agent)s, prefix: %(prefix)s)'
74 msg_args = {'agent': self.agent_id, 'prefix': self.prefix}
75 if self.started: 75 ↛ 80line 75 didn't jump to line 80 because the condition on line 75 was always true
76 self.coordinator.stop()
77 self.coordinator = None
78 self.started = False
80 LOG.info(msg, msg_args)
82 def get_lock(self, name):
83 """Return a Tooz back end lock.
85 :param str name: The lock name that is used to identify it
86 across all nodes.
87 """
88 # NOTE(gouthamr): Tooz expects lock name as a byte string
89 lock_name = (self.prefix + name).encode('ascii')
90 if self.started: 90 ↛ 93line 90 didn't jump to line 93 because the condition on line 90 was always true
91 return self.coordinator.get_lock(lock_name)
92 else:
93 raise exception.LockCreationFailed(_('Coordinator uninitialized.'))
96LOCK_COORDINATOR = Coordinator(prefix='manila-')
99class Lock(locking.Lock):
100 """Lock with dynamic name.
102 :param str lock_name: Lock name.
103 :param dict lock_data: Data for lock name formatting.
104 :param coordinator: Coordinator object to use when creating lock.
105 Defaults to the global coordinator.
107 Using it like so::
109 with Lock('mylock'):
110 ...
112 ensures that only one process at a time will execute code in context.
113 Lock name can be formatted using Python format string syntax::
115 Lock('foo-{share.id}, {'share': ...,}')
117 Available field names are keys of lock_data.
118 """
119 def __init__(self, lock_name, lock_data=None, coordinator=None):
120 super(Lock, self).__init__(str(id(self)))
121 lock_data = lock_data or {}
122 self.coordinator = coordinator or LOCK_COORDINATOR
123 self.blocking = True
124 self.lock = self._prepare_lock(lock_name, lock_data)
126 def _prepare_lock(self, lock_name, lock_data):
127 if not isinstance(lock_name, str): 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 raise ValueError(_('Not a valid string: %s') % lock_name)
129 return self.coordinator.get_lock(lock_name.format(**lock_data))
131 def acquire(self, blocking=None):
132 """Attempts to acquire lock.
134 :param blocking: If True, blocks until the lock is acquired. If False,
135 returns right away. Otherwise, the value is used as a timeout
136 value and the call returns maximum after this number of seconds.
137 :return: returns true if acquired (false if not)
138 :rtype: bool
139 """
140 blocking = self.blocking if blocking is None else blocking
141 return self.lock.acquire(blocking=blocking)
143 def release(self):
144 """Attempts to release lock.
146 The behavior of releasing a lock which was not acquired in the first
147 place is undefined.
148 """
149 self.lock.release()
152def synchronized(lock_name, blocking=True, coordinator=None):
153 """Synchronization decorator.
155 :param str lock_name: Lock name.
156 :param blocking: If True, blocks until the lock is acquired.
157 If False, raises exception when not acquired. Otherwise,
158 the value is used as a timeout value and if lock is not acquired
159 after this number of seconds exception is raised.
160 :param coordinator: Coordinator object to use when creating lock.
161 Defaults to the global coordinator.
162 :raises tooz.coordination.LockAcquireFailed: if lock is not acquired
164 Decorating a method like so::
166 @synchronized('mylock')
167 def foo(self, *args):
168 ...
170 ensures that only one process will execute the foo method at a time.
172 Different methods can share the same lock::
174 @synchronized('mylock')
175 def foo(self, *args):
176 ...
178 @synchronized('mylock')
179 def bar(self, *args):
180 ...
182 This way only one of either foo or bar can be executing at a time.
184 Lock name can be formatted using Python format string syntax::
186 @synchronized('{f_name}-{shr.id}-{snap[name]}')
187 def foo(self, shr, snap):
188 ...
190 Available field names are: decorated function parameters and
191 `f_name` as a decorated function name.
192 """
193 @decorator.decorator
194 def _synchronized(f, *a, **k):
195 call_args = inspect.getcallargs(f, *a, **k)
196 call_args['f_name'] = f.__name__
197 lock = Lock(lock_name, call_args, coordinator)
198 with lock(blocking):
199 LOG.debug('Lock "%(name)s" acquired by "%(function)s".',
200 {'name': lock_name, 'function': f.__name__})
201 return f(*a, **k)
202 return _synchronized