blob: a55fd9404722cc5647637875a918dc161c0afaf9 [file] [log] [blame]
Matthew Treinish0db53772013-07-26 10:39:35 -04001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack Foundation.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18
19import contextlib
20import errno
21import functools
22import os
Matthew Treinishffa94d62013-09-11 18:09:17 +000023import threading
Matthew Treinish0db53772013-07-26 10:39:35 -040024import time
25import weakref
26
Matthew Treinish968a3172013-10-09 18:20:57 -040027import fixtures
Matthew Treinish0db53772013-07-26 10:39:35 -040028from oslo.config import cfg
29
30from tempest.openstack.common import fileutils
31from tempest.openstack.common.gettextutils import _ # noqa
32from tempest.openstack.common import local
33from tempest.openstack.common import log as logging
34
35
36LOG = logging.getLogger(__name__)
37
38
39util_opts = [
40 cfg.BoolOpt('disable_process_locking', default=False,
41 help='Whether to disable inter-process locks'),
42 cfg.StrOpt('lock_path',
43 help=('Directory to use for lock files.'))
44]
45
46
47CONF = cfg.CONF
48CONF.register_opts(util_opts)
49
50
51def set_defaults(lock_path):
52 cfg.set_defaults(util_opts, lock_path=lock_path)
53
54
55class _InterProcessLock(object):
56 """Lock implementation which allows multiple locks, working around
57 issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
58 not require any cleanup. Since the lock is always held on a file
59 descriptor rather than outside of the process, the lock gets dropped
60 automatically if the process crashes, even if __exit__ is not executed.
61
62 There are no guarantees regarding usage by multiple green threads in a
63 single process here. This lock works only between processes. Exclusive
64 access between local threads should be achieved using the semaphores
65 in the @synchronized decorator.
66
67 Note these locks are released when the descriptor is closed, so it's not
68 safe to close the file descriptor while another green thread holds the
69 lock. Just opening and closing the lock file can break synchronisation,
70 so lock files must be accessed only using this abstraction.
71 """
72
73 def __init__(self, name):
74 self.lockfile = None
75 self.fname = name
76
77 def __enter__(self):
78 self.lockfile = open(self.fname, 'w')
79
80 while True:
81 try:
82 # Using non-blocking locks since green threads are not
83 # patched to deal with blocking locking calls.
84 # Also upon reading the MSDN docs for locking(), it seems
85 # to have a laughable 10 attempts "blocking" mechanism.
86 self.trylock()
87 return self
88 except IOError as e:
89 if e.errno in (errno.EACCES, errno.EAGAIN):
90 # external locks synchronise things like iptables
91 # updates - give it some time to prevent busy spinning
92 time.sleep(0.01)
93 else:
94 raise
95
96 def __exit__(self, exc_type, exc_val, exc_tb):
97 try:
98 self.unlock()
99 self.lockfile.close()
100 except IOError:
101 LOG.exception(_("Could not release the acquired lock `%s`"),
102 self.fname)
103
104 def trylock(self):
105 raise NotImplementedError()
106
107 def unlock(self):
108 raise NotImplementedError()
109
110
111class _WindowsLock(_InterProcessLock):
112 def trylock(self):
113 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
114
115 def unlock(self):
116 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
117
118
119class _PosixLock(_InterProcessLock):
120 def trylock(self):
121 fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
122
123 def unlock(self):
124 fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
125
126
127if os.name == 'nt':
128 import msvcrt
129 InterProcessLock = _WindowsLock
130else:
131 import fcntl
132 InterProcessLock = _PosixLock
133
134_semaphores = weakref.WeakValueDictionary()
135
136
137@contextlib.contextmanager
138def lock(name, lock_file_prefix=None, external=False, lock_path=None):
139 """Context based lock
140
Matthew Treinishffa94d62013-09-11 18:09:17 +0000141 This function yields a `threading.Semaphore` instance (if we don't use
142 eventlet.monkey_patch(), else `semaphore.Semaphore`) unless external is
Matthew Treinish0db53772013-07-26 10:39:35 -0400143 True, in which case, it'll yield an InterProcessLock instance.
144
145 :param lock_file_prefix: The lock_file_prefix argument is used to provide
146 lock files on disk with a meaningful prefix.
147
148 :param external: The external keyword argument denotes whether this lock
149 should work across multiple processes. This means that if two different
150 workers both run a a method decorated with @synchronized('mylock',
151 external=True), only one of them will execute at a time.
152
153 :param lock_path: The lock_path keyword argument is used to specify a
154 special location for external lock files to live. If nothing is set, then
155 CONF.lock_path is used as a default.
156 """
157 # NOTE(soren): If we ever go natively threaded, this will be racy.
158 # See http://stackoverflow.com/questions/5390569/dyn
159 # amically-allocating-and-destroying-mutexes
Matthew Treinishffa94d62013-09-11 18:09:17 +0000160 sem = _semaphores.get(name, threading.Semaphore())
Matthew Treinish0db53772013-07-26 10:39:35 -0400161 if name not in _semaphores:
162 # this check is not racy - we're already holding ref locally
163 # so GC won't remove the item and there was no IO switch
164 # (only valid in greenthreads)
165 _semaphores[name] = sem
166
167 with sem:
168 LOG.debug(_('Got semaphore "%(lock)s"'), {'lock': name})
169
170 # NOTE(mikal): I know this looks odd
171 if not hasattr(local.strong_store, 'locks_held'):
172 local.strong_store.locks_held = []
173 local.strong_store.locks_held.append(name)
174
175 try:
176 if external and not CONF.disable_process_locking:
177 LOG.debug(_('Attempting to grab file lock "%(lock)s"'),
178 {'lock': name})
179
180 # We need a copy of lock_path because it is non-local
181 local_lock_path = lock_path or CONF.lock_path
182 if not local_lock_path:
183 raise cfg.RequiredOptError('lock_path')
184
185 if not os.path.exists(local_lock_path):
186 fileutils.ensure_tree(local_lock_path)
187 LOG.info(_('Created lock path: %s'), local_lock_path)
188
189 def add_prefix(name, prefix):
190 if not prefix:
191 return name
192 sep = '' if prefix.endswith('-') else '-'
193 return '%s%s%s' % (prefix, sep, name)
194
195 # NOTE(mikal): the lock name cannot contain directory
196 # separators
197 lock_file_name = add_prefix(name.replace(os.sep, '_'),
198 lock_file_prefix)
199
200 lock_file_path = os.path.join(local_lock_path, lock_file_name)
201
202 try:
203 lock = InterProcessLock(lock_file_path)
204 with lock as lock:
205 LOG.debug(_('Got file lock "%(lock)s" at %(path)s'),
206 {'lock': name, 'path': lock_file_path})
207 yield lock
208 finally:
209 LOG.debug(_('Released file lock "%(lock)s" at %(path)s'),
210 {'lock': name, 'path': lock_file_path})
211 else:
212 yield sem
213
214 finally:
215 local.strong_store.locks_held.remove(name)
216
217
218def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
219 """Synchronization decorator.
220
221 Decorating a method like so::
222
223 @synchronized('mylock')
224 def foo(self, *args):
225 ...
226
227 ensures that only one thread will execute the foo method at a time.
228
229 Different methods can share the same lock::
230
231 @synchronized('mylock')
232 def foo(self, *args):
233 ...
234
235 @synchronized('mylock')
236 def bar(self, *args):
237 ...
238
239 This way only one of either foo or bar can be executing at a time.
240 """
241
242 def wrap(f):
243 @functools.wraps(f)
244 def inner(*args, **kwargs):
245 with lock(name, lock_file_prefix, external, lock_path):
246 LOG.debug(_('Got semaphore / lock "%(function)s"'),
247 {'function': f.__name__})
248 return f(*args, **kwargs)
249
250 LOG.debug(_('Semaphore / lock released "%(function)s"'),
251 {'function': f.__name__})
252 return inner
253 return wrap
254
255
256def synchronized_with_prefix(lock_file_prefix):
257 """Partial object generator for the synchronization decorator.
258
259 Redefine @synchronized in each project like so::
260
261 (in nova/utils.py)
262 from nova.openstack.common import lockutils
263
264 synchronized = lockutils.synchronized_with_prefix('nova-')
265
266
267 (in nova/foo.py)
268 from nova import utils
269
270 @utils.synchronized('mylock')
271 def bar(self, *args):
272 ...
273
274 The lock_file_prefix argument is used to provide lock files on disk with a
275 meaningful prefix.
276 """
277
278 return functools.partial(synchronized, lock_file_prefix=lock_file_prefix)
Matthew Treinish968a3172013-10-09 18:20:57 -0400279
280
281class LockFixture(fixtures.Fixture):
282 """External locking fixture.
283
284 This fixture is basically an alternative to the synchronized decorator with
285 the external flag so that tearDowns and addCleanups will be included in
286 the lock context for locking between tests. The fixture is recommended to
287 be the first line in a test method, like so::
288
289 def test_method(self):
290 self.useFixture(LockFixture)
291 ...
292
293 or the first line in setUp if all the test methods in the class are
294 required to be serialized. Something like::
295
296 class TestCase(testtools.testcase):
297 def setUp(self):
298 self.useFixture(LockFixture)
299 super(TestCase, self).setUp()
300 ...
301
302 This is because addCleanups are put on a LIFO queue that gets run after the
303 test method exits. (either by completing or raising an exception)
304 """
305 def __init__(self, name, lock_file_prefix=None):
306 self.mgr = lock(name, lock_file_prefix, True)
307
308 def setUp(self):
309 super(LockFixture, self).setUp()
310 self.addCleanup(self.mgr.__exit__, None, None, None)
311 self.mgr.__enter__()