blob: 65f35484771051f2dfa60edb99fe66a064dd6654 [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
Michael Stilla2b1dea2013-11-16 14:40:44 +110023import shutil
24import subprocess
25import sys
26import tempfile
Matthew Treinishffa94d62013-09-11 18:09:17 +000027import threading
Matthew Treinish0db53772013-07-26 10:39:35 -040028import time
29import weakref
30
Matthew Treinish0db53772013-07-26 10:39:35 -040031from oslo.config import cfg
32
33from tempest.openstack.common import fileutils
34from tempest.openstack.common.gettextutils import _ # noqa
35from tempest.openstack.common import local
36from tempest.openstack.common import log as logging
37
38
39LOG = logging.getLogger(__name__)
40
41
42util_opts = [
43 cfg.BoolOpt('disable_process_locking', default=False,
44 help='Whether to disable inter-process locks'),
45 cfg.StrOpt('lock_path',
Michael Stilla2b1dea2013-11-16 14:40:44 +110046 default=os.environ.get("TEMPEST_LOCK_PATH"),
Matthew Treinish0db53772013-07-26 10:39:35 -040047 help=('Directory to use for lock files.'))
48]
49
50
51CONF = cfg.CONF
52CONF.register_opts(util_opts)
53
54
55def set_defaults(lock_path):
56 cfg.set_defaults(util_opts, lock_path=lock_path)
57
58
59class _InterProcessLock(object):
60 """Lock implementation which allows multiple locks, working around
61 issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
62 not require any cleanup. Since the lock is always held on a file
63 descriptor rather than outside of the process, the lock gets dropped
64 automatically if the process crashes, even if __exit__ is not executed.
65
66 There are no guarantees regarding usage by multiple green threads in a
67 single process here. This lock works only between processes. Exclusive
68 access between local threads should be achieved using the semaphores
69 in the @synchronized decorator.
70
71 Note these locks are released when the descriptor is closed, so it's not
72 safe to close the file descriptor while another green thread holds the
73 lock. Just opening and closing the lock file can break synchronisation,
74 so lock files must be accessed only using this abstraction.
75 """
76
77 def __init__(self, name):
78 self.lockfile = None
79 self.fname = name
80
81 def __enter__(self):
82 self.lockfile = open(self.fname, 'w')
83
84 while True:
85 try:
86 # Using non-blocking locks since green threads are not
87 # patched to deal with blocking locking calls.
88 # Also upon reading the MSDN docs for locking(), it seems
89 # to have a laughable 10 attempts "blocking" mechanism.
90 self.trylock()
91 return self
92 except IOError as e:
93 if e.errno in (errno.EACCES, errno.EAGAIN):
94 # external locks synchronise things like iptables
95 # updates - give it some time to prevent busy spinning
96 time.sleep(0.01)
97 else:
98 raise
99
100 def __exit__(self, exc_type, exc_val, exc_tb):
101 try:
102 self.unlock()
103 self.lockfile.close()
104 except IOError:
105 LOG.exception(_("Could not release the acquired lock `%s`"),
106 self.fname)
107
108 def trylock(self):
109 raise NotImplementedError()
110
111 def unlock(self):
112 raise NotImplementedError()
113
114
115class _WindowsLock(_InterProcessLock):
116 def trylock(self):
117 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
118
119 def unlock(self):
120 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
121
122
123class _PosixLock(_InterProcessLock):
124 def trylock(self):
125 fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
126
127 def unlock(self):
128 fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
129
130
131if os.name == 'nt':
132 import msvcrt
133 InterProcessLock = _WindowsLock
134else:
135 import fcntl
136 InterProcessLock = _PosixLock
137
138_semaphores = weakref.WeakValueDictionary()
Michael Stilla2b1dea2013-11-16 14:40:44 +1100139_semaphores_lock = threading.Lock()
Matthew Treinish0db53772013-07-26 10:39:35 -0400140
141
142@contextlib.contextmanager
143def lock(name, lock_file_prefix=None, external=False, lock_path=None):
144 """Context based lock
145
Matthew Treinishffa94d62013-09-11 18:09:17 +0000146 This function yields a `threading.Semaphore` instance (if we don't use
147 eventlet.monkey_patch(), else `semaphore.Semaphore`) unless external is
Matthew Treinish0db53772013-07-26 10:39:35 -0400148 True, in which case, it'll yield an InterProcessLock instance.
149
150 :param lock_file_prefix: The lock_file_prefix argument is used to provide
151 lock files on disk with a meaningful prefix.
152
153 :param external: The external keyword argument denotes whether this lock
154 should work across multiple processes. This means that if two different
155 workers both run a a method decorated with @synchronized('mylock',
156 external=True), only one of them will execute at a time.
157
158 :param lock_path: The lock_path keyword argument is used to specify a
159 special location for external lock files to live. If nothing is set, then
160 CONF.lock_path is used as a default.
161 """
Michael Stilla2b1dea2013-11-16 14:40:44 +1100162 with _semaphores_lock:
163 try:
164 sem = _semaphores[name]
165 except KeyError:
166 sem = threading.Semaphore()
167 _semaphores[name] = sem
Matthew Treinish0db53772013-07-26 10:39:35 -0400168
169 with sem:
170 LOG.debug(_('Got semaphore "%(lock)s"'), {'lock': name})
171
172 # NOTE(mikal): I know this looks odd
173 if not hasattr(local.strong_store, 'locks_held'):
174 local.strong_store.locks_held = []
175 local.strong_store.locks_held.append(name)
176
177 try:
178 if external and not CONF.disable_process_locking:
179 LOG.debug(_('Attempting to grab file lock "%(lock)s"'),
180 {'lock': name})
181
182 # We need a copy of lock_path because it is non-local
183 local_lock_path = lock_path or CONF.lock_path
184 if not local_lock_path:
185 raise cfg.RequiredOptError('lock_path')
186
187 if not os.path.exists(local_lock_path):
188 fileutils.ensure_tree(local_lock_path)
189 LOG.info(_('Created lock path: %s'), local_lock_path)
190
191 def add_prefix(name, prefix):
192 if not prefix:
193 return name
194 sep = '' if prefix.endswith('-') else '-'
195 return '%s%s%s' % (prefix, sep, name)
196
197 # NOTE(mikal): the lock name cannot contain directory
198 # separators
199 lock_file_name = add_prefix(name.replace(os.sep, '_'),
200 lock_file_prefix)
201
202 lock_file_path = os.path.join(local_lock_path, lock_file_name)
203
204 try:
205 lock = InterProcessLock(lock_file_path)
206 with lock as lock:
207 LOG.debug(_('Got file lock "%(lock)s" at %(path)s'),
208 {'lock': name, 'path': lock_file_path})
209 yield lock
210 finally:
211 LOG.debug(_('Released file lock "%(lock)s" at %(path)s'),
212 {'lock': name, 'path': lock_file_path})
213 else:
214 yield sem
215
216 finally:
217 local.strong_store.locks_held.remove(name)
218
219
220def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
221 """Synchronization decorator.
222
223 Decorating a method like so::
224
225 @synchronized('mylock')
226 def foo(self, *args):
227 ...
228
229 ensures that only one thread will execute the foo method at a time.
230
231 Different methods can share the same lock::
232
233 @synchronized('mylock')
234 def foo(self, *args):
235 ...
236
237 @synchronized('mylock')
238 def bar(self, *args):
239 ...
240
241 This way only one of either foo or bar can be executing at a time.
242 """
243
244 def wrap(f):
245 @functools.wraps(f)
246 def inner(*args, **kwargs):
Matthew Treinishf45528a2013-10-24 20:12:28 +0000247 try:
248 with lock(name, lock_file_prefix, external, lock_path):
249 LOG.debug(_('Got semaphore / lock "%(function)s"'),
250 {'function': f.__name__})
251 return f(*args, **kwargs)
252 finally:
253 LOG.debug(_('Semaphore / lock released "%(function)s"'),
Matthew Treinish0db53772013-07-26 10:39:35 -0400254 {'function': f.__name__})
Matthew Treinish0db53772013-07-26 10:39:35 -0400255 return inner
256 return wrap
257
258
259def synchronized_with_prefix(lock_file_prefix):
260 """Partial object generator for the synchronization decorator.
261
262 Redefine @synchronized in each project like so::
263
264 (in nova/utils.py)
265 from nova.openstack.common import lockutils
266
267 synchronized = lockutils.synchronized_with_prefix('nova-')
268
269
270 (in nova/foo.py)
271 from nova import utils
272
273 @utils.synchronized('mylock')
274 def bar(self, *args):
275 ...
276
277 The lock_file_prefix argument is used to provide lock files on disk with a
278 meaningful prefix.
279 """
280
281 return functools.partial(synchronized, lock_file_prefix=lock_file_prefix)
Michael Stilla2b1dea2013-11-16 14:40:44 +1100282
283
284def main(argv):
285 """Create a dir for locks and pass it to command from arguments
286
287 If you run this:
288 python -m openstack.common.lockutils python setup.py testr <etc>
289
290 a temporary directory will be created for all your locks and passed to all
291 your tests in an environment variable. The temporary dir will be deleted
292 afterwards and the return value will be preserved.
293 """
294
295 lock_dir = tempfile.mkdtemp()
296 os.environ["TEMPEST_LOCK_PATH"] = lock_dir
297 try:
298 ret_val = subprocess.call(argv[1:])
299 finally:
300 shutil.rmtree(lock_dir, ignore_errors=True)
301 return ret_val
302
303
304if __name__ == '__main__':
305 sys.exit(main(sys.argv))