blob: 3ff1a7ad50a0d623303090ea6a63fd308995bff8 [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
23import time
24import weakref
25
26from eventlet import semaphore
27from 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
140 This function yields a `semaphore.Semaphore` instance unless external is
141 True, in which case, it'll yield an InterProcessLock instance.
142
143 :param lock_file_prefix: The lock_file_prefix argument is used to provide
144 lock files on disk with a meaningful prefix.
145
146 :param external: The external keyword argument denotes whether this lock
147 should work across multiple processes. This means that if two different
148 workers both run a a method decorated with @synchronized('mylock',
149 external=True), only one of them will execute at a time.
150
151 :param lock_path: The lock_path keyword argument is used to specify a
152 special location for external lock files to live. If nothing is set, then
153 CONF.lock_path is used as a default.
154 """
155 # NOTE(soren): If we ever go natively threaded, this will be racy.
156 # See http://stackoverflow.com/questions/5390569/dyn
157 # amically-allocating-and-destroying-mutexes
158 sem = _semaphores.get(name, semaphore.Semaphore())
159 if name not in _semaphores:
160 # this check is not racy - we're already holding ref locally
161 # so GC won't remove the item and there was no IO switch
162 # (only valid in greenthreads)
163 _semaphores[name] = sem
164
165 with sem:
166 LOG.debug(_('Got semaphore "%(lock)s"'), {'lock': name})
167
168 # NOTE(mikal): I know this looks odd
169 if not hasattr(local.strong_store, 'locks_held'):
170 local.strong_store.locks_held = []
171 local.strong_store.locks_held.append(name)
172
173 try:
174 if external and not CONF.disable_process_locking:
175 LOG.debug(_('Attempting to grab file lock "%(lock)s"'),
176 {'lock': name})
177
178 # We need a copy of lock_path because it is non-local
179 local_lock_path = lock_path or CONF.lock_path
180 if not local_lock_path:
181 raise cfg.RequiredOptError('lock_path')
182
183 if not os.path.exists(local_lock_path):
184 fileutils.ensure_tree(local_lock_path)
185 LOG.info(_('Created lock path: %s'), local_lock_path)
186
187 def add_prefix(name, prefix):
188 if not prefix:
189 return name
190 sep = '' if prefix.endswith('-') else '-'
191 return '%s%s%s' % (prefix, sep, name)
192
193 # NOTE(mikal): the lock name cannot contain directory
194 # separators
195 lock_file_name = add_prefix(name.replace(os.sep, '_'),
196 lock_file_prefix)
197
198 lock_file_path = os.path.join(local_lock_path, lock_file_name)
199
200 try:
201 lock = InterProcessLock(lock_file_path)
202 with lock as lock:
203 LOG.debug(_('Got file lock "%(lock)s" at %(path)s'),
204 {'lock': name, 'path': lock_file_path})
205 yield lock
206 finally:
207 LOG.debug(_('Released file lock "%(lock)s" at %(path)s'),
208 {'lock': name, 'path': lock_file_path})
209 else:
210 yield sem
211
212 finally:
213 local.strong_store.locks_held.remove(name)
214
215
216def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
217 """Synchronization decorator.
218
219 Decorating a method like so::
220
221 @synchronized('mylock')
222 def foo(self, *args):
223 ...
224
225 ensures that only one thread will execute the foo method at a time.
226
227 Different methods can share the same lock::
228
229 @synchronized('mylock')
230 def foo(self, *args):
231 ...
232
233 @synchronized('mylock')
234 def bar(self, *args):
235 ...
236
237 This way only one of either foo or bar can be executing at a time.
238 """
239
240 def wrap(f):
241 @functools.wraps(f)
242 def inner(*args, **kwargs):
243 with lock(name, lock_file_prefix, external, lock_path):
244 LOG.debug(_('Got semaphore / lock "%(function)s"'),
245 {'function': f.__name__})
246 return f(*args, **kwargs)
247
248 LOG.debug(_('Semaphore / lock released "%(function)s"'),
249 {'function': f.__name__})
250 return inner
251 return wrap
252
253
254def synchronized_with_prefix(lock_file_prefix):
255 """Partial object generator for the synchronization decorator.
256
257 Redefine @synchronized in each project like so::
258
259 (in nova/utils.py)
260 from nova.openstack.common import lockutils
261
262 synchronized = lockutils.synchronized_with_prefix('nova-')
263
264
265 (in nova/foo.py)
266 from nova import utils
267
268 @utils.synchronized('mylock')
269 def bar(self, *args):
270 ...
271
272 The lock_file_prefix argument is used to provide lock files on disk with a
273 meaningful prefix.
274 """
275
276 return functools.partial(synchronized, lock_file_prefix=lock_file_prefix)