blob: 8ea8766e71b8cb04521c40de0132f01d05be860a [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 Treinish0db53772013-07-26 10:39:35 -040027from oslo.config import cfg
28
29from tempest.openstack.common import fileutils
30from tempest.openstack.common.gettextutils import _ # noqa
31from tempest.openstack.common import local
32from tempest.openstack.common import log as logging
33
34
35LOG = logging.getLogger(__name__)
36
37
38util_opts = [
39 cfg.BoolOpt('disable_process_locking', default=False,
40 help='Whether to disable inter-process locks'),
41 cfg.StrOpt('lock_path',
42 help=('Directory to use for lock files.'))
43]
44
45
46CONF = cfg.CONF
47CONF.register_opts(util_opts)
48
49
50def set_defaults(lock_path):
51 cfg.set_defaults(util_opts, lock_path=lock_path)
52
53
54class _InterProcessLock(object):
55 """Lock implementation which allows multiple locks, working around
56 issues like bugs.debian.org/cgi-bin/bugreport.cgi?bug=632857 and does
57 not require any cleanup. Since the lock is always held on a file
58 descriptor rather than outside of the process, the lock gets dropped
59 automatically if the process crashes, even if __exit__ is not executed.
60
61 There are no guarantees regarding usage by multiple green threads in a
62 single process here. This lock works only between processes. Exclusive
63 access between local threads should be achieved using the semaphores
64 in the @synchronized decorator.
65
66 Note these locks are released when the descriptor is closed, so it's not
67 safe to close the file descriptor while another green thread holds the
68 lock. Just opening and closing the lock file can break synchronisation,
69 so lock files must be accessed only using this abstraction.
70 """
71
72 def __init__(self, name):
73 self.lockfile = None
74 self.fname = name
75
76 def __enter__(self):
77 self.lockfile = open(self.fname, 'w')
78
79 while True:
80 try:
81 # Using non-blocking locks since green threads are not
82 # patched to deal with blocking locking calls.
83 # Also upon reading the MSDN docs for locking(), it seems
84 # to have a laughable 10 attempts "blocking" mechanism.
85 self.trylock()
86 return self
87 except IOError as e:
88 if e.errno in (errno.EACCES, errno.EAGAIN):
89 # external locks synchronise things like iptables
90 # updates - give it some time to prevent busy spinning
91 time.sleep(0.01)
92 else:
93 raise
94
95 def __exit__(self, exc_type, exc_val, exc_tb):
96 try:
97 self.unlock()
98 self.lockfile.close()
99 except IOError:
100 LOG.exception(_("Could not release the acquired lock `%s`"),
101 self.fname)
102
103 def trylock(self):
104 raise NotImplementedError()
105
106 def unlock(self):
107 raise NotImplementedError()
108
109
110class _WindowsLock(_InterProcessLock):
111 def trylock(self):
112 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
113
114 def unlock(self):
115 msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
116
117
118class _PosixLock(_InterProcessLock):
119 def trylock(self):
120 fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
121
122 def unlock(self):
123 fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
124
125
126if os.name == 'nt':
127 import msvcrt
128 InterProcessLock = _WindowsLock
129else:
130 import fcntl
131 InterProcessLock = _PosixLock
132
133_semaphores = weakref.WeakValueDictionary()
134
135
136@contextlib.contextmanager
137def lock(name, lock_file_prefix=None, external=False, lock_path=None):
138 """Context based lock
139
Matthew Treinishffa94d62013-09-11 18:09:17 +0000140 This function yields a `threading.Semaphore` instance (if we don't use
141 eventlet.monkey_patch(), else `semaphore.Semaphore`) unless external is
Matthew Treinish0db53772013-07-26 10:39:35 -0400142 True, in which case, it'll yield an InterProcessLock instance.
143
144 :param lock_file_prefix: The lock_file_prefix argument is used to provide
145 lock files on disk with a meaningful prefix.
146
147 :param external: The external keyword argument denotes whether this lock
148 should work across multiple processes. This means that if two different
149 workers both run a a method decorated with @synchronized('mylock',
150 external=True), only one of them will execute at a time.
151
152 :param lock_path: The lock_path keyword argument is used to specify a
153 special location for external lock files to live. If nothing is set, then
154 CONF.lock_path is used as a default.
155 """
156 # NOTE(soren): If we ever go natively threaded, this will be racy.
157 # See http://stackoverflow.com/questions/5390569/dyn
158 # amically-allocating-and-destroying-mutexes
Matthew Treinishffa94d62013-09-11 18:09:17 +0000159 sem = _semaphores.get(name, threading.Semaphore())
Matthew Treinish0db53772013-07-26 10:39:35 -0400160 if name not in _semaphores:
161 # this check is not racy - we're already holding ref locally
162 # so GC won't remove the item and there was no IO switch
163 # (only valid in greenthreads)
164 _semaphores[name] = sem
165
166 with sem:
167 LOG.debug(_('Got semaphore "%(lock)s"'), {'lock': name})
168
169 # NOTE(mikal): I know this looks odd
170 if not hasattr(local.strong_store, 'locks_held'):
171 local.strong_store.locks_held = []
172 local.strong_store.locks_held.append(name)
173
174 try:
175 if external and not CONF.disable_process_locking:
176 LOG.debug(_('Attempting to grab file lock "%(lock)s"'),
177 {'lock': name})
178
179 # We need a copy of lock_path because it is non-local
180 local_lock_path = lock_path or CONF.lock_path
181 if not local_lock_path:
182 raise cfg.RequiredOptError('lock_path')
183
184 if not os.path.exists(local_lock_path):
185 fileutils.ensure_tree(local_lock_path)
186 LOG.info(_('Created lock path: %s'), local_lock_path)
187
188 def add_prefix(name, prefix):
189 if not prefix:
190 return name
191 sep = '' if prefix.endswith('-') else '-'
192 return '%s%s%s' % (prefix, sep, name)
193
194 # NOTE(mikal): the lock name cannot contain directory
195 # separators
196 lock_file_name = add_prefix(name.replace(os.sep, '_'),
197 lock_file_prefix)
198
199 lock_file_path = os.path.join(local_lock_path, lock_file_name)
200
201 try:
202 lock = InterProcessLock(lock_file_path)
203 with lock as lock:
204 LOG.debug(_('Got file lock "%(lock)s" at %(path)s'),
205 {'lock': name, 'path': lock_file_path})
206 yield lock
207 finally:
208 LOG.debug(_('Released file lock "%(lock)s" at %(path)s'),
209 {'lock': name, 'path': lock_file_path})
210 else:
211 yield sem
212
213 finally:
214 local.strong_store.locks_held.remove(name)
215
216
217def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
218 """Synchronization decorator.
219
220 Decorating a method like so::
221
222 @synchronized('mylock')
223 def foo(self, *args):
224 ...
225
226 ensures that only one thread will execute the foo method at a time.
227
228 Different methods can share the same lock::
229
230 @synchronized('mylock')
231 def foo(self, *args):
232 ...
233
234 @synchronized('mylock')
235 def bar(self, *args):
236 ...
237
238 This way only one of either foo or bar can be executing at a time.
239 """
240
241 def wrap(f):
242 @functools.wraps(f)
243 def inner(*args, **kwargs):
Matthew Treinishf45528a2013-10-24 20:12:28 +0000244 try:
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 finally:
250 LOG.debug(_('Semaphore / lock released "%(function)s"'),
Matthew Treinish0db53772013-07-26 10:39:35 -0400251 {'function': f.__name__})
Matthew Treinish0db53772013-07-26 10:39:35 -0400252 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)