~ubuntu-branches/ubuntu/vivid/ironic/vivid-updates

« back to all changes in this revision

Viewing changes to ironic/openstack/common/lockutils.py

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2015-01-05 12:21:37 UTC
  • mfrom: (1.2.4)
  • Revision ID: package-import@ubuntu.com-20150105122137-171bqrdpcxqipunk
Tags: 2015.1~b1-0ubuntu1
* New upstream beta release:
  - d/control: Align version requirements with upstream release.
* d/watch: Update uversionmangle to deal with kilo beta versioning
  changes.
* d/control: Bumped Standards-Version to 3.9.6, no changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2011 OpenStack Foundation.
2
 
# All Rights Reserved.
3
 
#
4
 
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
 
#    not use this file except in compliance with the License. You may obtain
6
 
#    a copy of the License at
7
 
#
8
 
#         http://www.apache.org/licenses/LICENSE-2.0
9
 
#
10
 
#    Unless required by applicable law or agreed to in writing, software
11
 
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
 
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
 
#    License for the specific language governing permissions and limitations
14
 
#    under the License.
15
 
 
16
 
import contextlib
17
 
import errno
18
 
import functools
19
 
import logging
20
 
import os
21
 
import shutil
22
 
import subprocess
23
 
import sys
24
 
import tempfile
25
 
import threading
26
 
import time
27
 
import weakref
28
 
 
29
 
from oslo.config import cfg
30
 
 
31
 
from ironic.openstack.common import fileutils
32
 
from ironic.openstack.common.gettextutils import _, _LE, _LI
33
 
 
34
 
 
35
 
LOG = logging.getLogger(__name__)
36
 
 
37
 
 
38
 
util_opts = [
39
 
    cfg.BoolOpt('disable_process_locking', default=False,
40
 
                help='Enables or disables inter-process locks.'),
41
 
    cfg.StrOpt('lock_path',
42
 
               default=os.environ.get("IRONIC_LOCK_PATH"),
43
 
               help='Directory to use for lock files.')
44
 
]
45
 
 
46
 
 
47
 
CONF = cfg.CONF
48
 
CONF.register_opts(util_opts)
49
 
 
50
 
 
51
 
def set_defaults(lock_path):
52
 
    cfg.set_defaults(util_opts, lock_path=lock_path)
53
 
 
54
 
 
55
 
class _FileLock(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 acquire(self):
78
 
        basedir = os.path.dirname(self.fname)
79
 
 
80
 
        if not os.path.exists(basedir):
81
 
            fileutils.ensure_tree(basedir)
82
 
            LOG.info(_LI('Created lock path: %s'), basedir)
83
 
 
84
 
        self.lockfile = open(self.fname, 'w')
85
 
 
86
 
        while True:
87
 
            try:
88
 
                # Using non-blocking locks since green threads are not
89
 
                # patched to deal with blocking locking calls.
90
 
                # Also upon reading the MSDN docs for locking(), it seems
91
 
                # to have a laughable 10 attempts "blocking" mechanism.
92
 
                self.trylock()
93
 
                LOG.debug('Got file lock "%s"', self.fname)
94
 
                return True
95
 
            except IOError as e:
96
 
                if e.errno in (errno.EACCES, errno.EAGAIN):
97
 
                    # external locks synchronise things like iptables
98
 
                    # updates - give it some time to prevent busy spinning
99
 
                    time.sleep(0.01)
100
 
                else:
101
 
                    raise threading.ThreadError(_("Unable to acquire lock on"
102
 
                                                  " `%(filename)s` due to"
103
 
                                                  " %(exception)s") %
104
 
                                                {
105
 
                                                    'filename': self.fname,
106
 
                                                    'exception': e,
107
 
                                                })
108
 
 
109
 
    def __enter__(self):
110
 
        self.acquire()
111
 
        return self
112
 
 
113
 
    def release(self):
114
 
        try:
115
 
            self.unlock()
116
 
            self.lockfile.close()
117
 
            LOG.debug('Released file lock "%s"', self.fname)
118
 
        except IOError:
119
 
            LOG.exception(_LE("Could not release the acquired lock `%s`"),
120
 
                          self.fname)
121
 
 
122
 
    def __exit__(self, exc_type, exc_val, exc_tb):
123
 
        self.release()
124
 
 
125
 
    def exists(self):
126
 
        return os.path.exists(self.fname)
127
 
 
128
 
    def trylock(self):
129
 
        raise NotImplementedError()
130
 
 
131
 
    def unlock(self):
132
 
        raise NotImplementedError()
133
 
 
134
 
 
135
 
class _WindowsLock(_FileLock):
136
 
    def trylock(self):
137
 
        msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_NBLCK, 1)
138
 
 
139
 
    def unlock(self):
140
 
        msvcrt.locking(self.lockfile.fileno(), msvcrt.LK_UNLCK, 1)
141
 
 
142
 
 
143
 
class _FcntlLock(_FileLock):
144
 
    def trylock(self):
145
 
        fcntl.lockf(self.lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
146
 
 
147
 
    def unlock(self):
148
 
        fcntl.lockf(self.lockfile, fcntl.LOCK_UN)
149
 
 
150
 
 
151
 
class _PosixLock(object):
152
 
    def __init__(self, name):
153
 
        # Hash the name because it's not valid to have POSIX semaphore
154
 
        # names with things like / in them. Then use base64 to encode
155
 
        # the digest() instead taking the hexdigest() because the
156
 
        # result is shorter and most systems can't have shm sempahore
157
 
        # names longer than 31 characters.
158
 
        h = hashlib.sha1()
159
 
        h.update(name.encode('ascii'))
160
 
        self.name = str((b'/' + base64.urlsafe_b64encode(
161
 
            h.digest())).decode('ascii'))
162
 
 
163
 
    def acquire(self, timeout=None):
164
 
        self.semaphore = posix_ipc.Semaphore(self.name,
165
 
                                             flags=posix_ipc.O_CREAT,
166
 
                                             initial_value=1)
167
 
        self.semaphore.acquire(timeout)
168
 
        return self
169
 
 
170
 
    def __enter__(self):
171
 
        self.acquire()
172
 
        return self
173
 
 
174
 
    def release(self):
175
 
        self.semaphore.release()
176
 
        self.semaphore.close()
177
 
 
178
 
    def __exit__(self, exc_type, exc_val, exc_tb):
179
 
        self.release()
180
 
 
181
 
    def exists(self):
182
 
        try:
183
 
            semaphore = posix_ipc.Semaphore(self.name)
184
 
        except posix_ipc.ExistentialError:
185
 
            return False
186
 
        else:
187
 
            semaphore.close()
188
 
        return True
189
 
 
190
 
 
191
 
if os.name == 'nt':
192
 
    import msvcrt
193
 
    InterProcessLock = _WindowsLock
194
 
    FileLock = _WindowsLock
195
 
else:
196
 
    import base64
197
 
    import fcntl
198
 
    import hashlib
199
 
 
200
 
    import posix_ipc
201
 
    InterProcessLock = _PosixLock
202
 
    FileLock = _FcntlLock
203
 
 
204
 
_semaphores = weakref.WeakValueDictionary()
205
 
_semaphores_lock = threading.Lock()
206
 
 
207
 
 
208
 
def _get_lock_path(name, lock_file_prefix, lock_path=None):
209
 
    # NOTE(mikal): the lock name cannot contain directory
210
 
    # separators
211
 
    name = name.replace(os.sep, '_')
212
 
    if lock_file_prefix:
213
 
        sep = '' if lock_file_prefix.endswith('-') else '-'
214
 
        name = '%s%s%s' % (lock_file_prefix, sep, name)
215
 
 
216
 
    local_lock_path = lock_path or CONF.lock_path
217
 
 
218
 
    if not local_lock_path:
219
 
        # NOTE(bnemec): Create a fake lock path for posix locks so we don't
220
 
        # unnecessarily raise the RequiredOptError below.
221
 
        if InterProcessLock is not _PosixLock:
222
 
            raise cfg.RequiredOptError('lock_path')
223
 
        local_lock_path = 'posixlock:/'
224
 
 
225
 
    return os.path.join(local_lock_path, name)
226
 
 
227
 
 
228
 
def external_lock(name, lock_file_prefix=None, lock_path=None):
229
 
    LOG.debug('Attempting to grab external lock "%(lock)s"',
230
 
              {'lock': name})
231
 
 
232
 
    lock_file_path = _get_lock_path(name, lock_file_prefix, lock_path)
233
 
 
234
 
    # NOTE(bnemec): If an explicit lock_path was passed to us then it
235
 
    # means the caller is relying on file-based locking behavior, so
236
 
    # we can't use posix locks for those calls.
237
 
    if lock_path:
238
 
        return FileLock(lock_file_path)
239
 
    return InterProcessLock(lock_file_path)
240
 
 
241
 
 
242
 
def remove_external_lock_file(name, lock_file_prefix=None):
243
 
    """Remove an external lock file when it's not used anymore
244
 
    This will be helpful when we have a lot of lock files
245
 
    """
246
 
    with internal_lock(name):
247
 
        lock_file_path = _get_lock_path(name, lock_file_prefix)
248
 
        try:
249
 
            os.remove(lock_file_path)
250
 
        except OSError:
251
 
            LOG.info(_LI('Failed to remove file %(file)s'),
252
 
                     {'file': lock_file_path})
253
 
 
254
 
 
255
 
def internal_lock(name):
256
 
    with _semaphores_lock:
257
 
        try:
258
 
            sem = _semaphores[name]
259
 
        except KeyError:
260
 
            sem = threading.Semaphore()
261
 
            _semaphores[name] = sem
262
 
 
263
 
    LOG.debug('Got semaphore "%(lock)s"', {'lock': name})
264
 
    return sem
265
 
 
266
 
 
267
 
@contextlib.contextmanager
268
 
def lock(name, lock_file_prefix=None, external=False, lock_path=None):
269
 
    """Context based lock
270
 
 
271
 
    This function yields a `threading.Semaphore` instance (if we don't use
272
 
    eventlet.monkey_patch(), else `semaphore.Semaphore`) unless external is
273
 
    True, in which case, it'll yield an InterProcessLock instance.
274
 
 
275
 
    :param lock_file_prefix: The lock_file_prefix argument is used to provide
276
 
      lock files on disk with a meaningful prefix.
277
 
 
278
 
    :param external: The external keyword argument denotes whether this lock
279
 
      should work across multiple processes. This means that if two different
280
 
      workers both run a method decorated with @synchronized('mylock',
281
 
      external=True), only one of them will execute at a time.
282
 
    """
283
 
    int_lock = internal_lock(name)
284
 
    with int_lock:
285
 
        if external and not CONF.disable_process_locking:
286
 
            ext_lock = external_lock(name, lock_file_prefix, lock_path)
287
 
            with ext_lock:
288
 
                yield ext_lock
289
 
        else:
290
 
            yield int_lock
291
 
    LOG.debug('Released semaphore "%(lock)s"', {'lock': name})
292
 
 
293
 
 
294
 
def synchronized(name, lock_file_prefix=None, external=False, lock_path=None):
295
 
    """Synchronization decorator.
296
 
 
297
 
    Decorating a method like so::
298
 
 
299
 
        @synchronized('mylock')
300
 
        def foo(self, *args):
301
 
           ...
302
 
 
303
 
    ensures that only one thread will execute the foo method at a time.
304
 
 
305
 
    Different methods can share the same lock::
306
 
 
307
 
        @synchronized('mylock')
308
 
        def foo(self, *args):
309
 
           ...
310
 
 
311
 
        @synchronized('mylock')
312
 
        def bar(self, *args):
313
 
           ...
314
 
 
315
 
    This way only one of either foo or bar can be executing at a time.
316
 
    """
317
 
 
318
 
    def wrap(f):
319
 
        @functools.wraps(f)
320
 
        def inner(*args, **kwargs):
321
 
            try:
322
 
                with lock(name, lock_file_prefix, external, lock_path):
323
 
                    LOG.debug('Got semaphore / lock "%(function)s"',
324
 
                              {'function': f.__name__})
325
 
                    return f(*args, **kwargs)
326
 
            finally:
327
 
                LOG.debug('Semaphore / lock released "%(function)s"',
328
 
                          {'function': f.__name__})
329
 
        return inner
330
 
    return wrap
331
 
 
332
 
 
333
 
def synchronized_with_prefix(lock_file_prefix):
334
 
    """Partial object generator for the synchronization decorator.
335
 
 
336
 
    Redefine @synchronized in each project like so::
337
 
 
338
 
        (in nova/utils.py)
339
 
        from nova.openstack.common import lockutils
340
 
 
341
 
        synchronized = lockutils.synchronized_with_prefix('nova-')
342
 
 
343
 
 
344
 
        (in nova/foo.py)
345
 
        from nova import utils
346
 
 
347
 
        @utils.synchronized('mylock')
348
 
        def bar(self, *args):
349
 
           ...
350
 
 
351
 
    The lock_file_prefix argument is used to provide lock files on disk with a
352
 
    meaningful prefix.
353
 
    """
354
 
 
355
 
    return functools.partial(synchronized, lock_file_prefix=lock_file_prefix)
356
 
 
357
 
 
358
 
def main(argv):
359
 
    """Create a dir for locks and pass it to command from arguments
360
 
 
361
 
    If you run this:
362
 
    python -m openstack.common.lockutils python setup.py testr <etc>
363
 
 
364
 
    a temporary directory will be created for all your locks and passed to all
365
 
    your tests in an environment variable. The temporary dir will be deleted
366
 
    afterwards and the return value will be preserved.
367
 
    """
368
 
 
369
 
    lock_dir = tempfile.mkdtemp()
370
 
    os.environ["IRONIC_LOCK_PATH"] = lock_dir
371
 
    try:
372
 
        ret_val = subprocess.call(argv[1:])
373
 
    finally:
374
 
        shutil.rmtree(lock_dir, ignore_errors=True)
375
 
    return ret_val
376
 
 
377
 
 
378
 
if __name__ == '__main__':
379
 
    sys.exit(main(sys.argv))