summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/tests/test_misc.py34
-rw-r--r--nova/utils.py67
2 files changed, 92 insertions, 9 deletions
diff --git a/nova/tests/test_misc.py b/nova/tests/test_misc.py
index 1fbaf304f..961499a60 100644
--- a/nova/tests/test_misc.py
+++ b/nova/tests/test_misc.py
@@ -16,8 +16,12 @@
import errno
import os
+import random
import select
+from eventlet import greenpool
+from eventlet import greenthread
+
from nova import test
from nova.utils import parse_mailmap, str_dict_replace, synchronized
@@ -72,11 +76,37 @@ class LockTestCase(test.TestCase):
self.assertEquals(foo.__name__, 'foo', "Wrapped function's name "
"got mangled")
- def test_synchronized(self):
+ def test_synchronized_internally(self):
+ """We can lock across multiple green threads"""
+ seen_threads = list()
+ @synchronized('testlock', external=False)
+ def f(id):
+ for x in range(10):
+ seen_threads.append(id)
+ greenthread.sleep(0)
+
+ threads = []
+ pool = greenpool.GreenPool(10)
+ for i in range(10):
+ threads.append(pool.spawn(f, i))
+
+ for thread in threads:
+ thread.wait()
+
+ self.assertEquals(len(seen_threads), 100)
+ # Looking at the seen threads, split it into chunks of 10, and verify
+ # that the last 9 match the first in each chunk.
+ for i in range(10):
+ for j in range(9):
+ self.assertEquals(seen_threads[i*10], seen_threads[i*10+1+j])
+
+
+ def test_synchronized_externally(self):
+ """We can lock across multiple processes"""
rpipe1, wpipe1 = os.pipe()
rpipe2, wpipe2 = os.pipe()
- @synchronized('testlock')
+ @synchronized('testlock', external=True)
def f(rpipe, wpipe):
try:
os.write(wpipe, "foo")
diff --git a/nova/utils.py b/nova/utils.py
index 499af2039..8936614cc 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -41,6 +41,7 @@ from xml.sax import saxutils
from eventlet import event
from eventlet import greenthread
+from eventlet import semaphore
from eventlet.green import subprocess
None
from nova import exception
@@ -531,17 +532,69 @@ def loads(s):
return json.loads(s)
-def synchronized(name):
+_semaphores_semaphore = semaphore.Semaphore()
+_semaphores = {}
+
+
+class _NoopContextManager(object):
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+
+def synchronized(name, external=False):
+ """Synchronization decorator
+
+ Decorating a method like so:
+ @synchronized('mylock')
+ def foo(self, *args):
+ ...
+
+ ensures that only one thread will execute the bar method at a time.
+
+ Different methods can share the same lock:
+ @synchronized('mylock')
+ def foo(self, *args):
+ ...
+
+ @synchronized('mylock')
+ def bar(self, *args):
+ ...
+
+ This way only one of either foo or bar can be executing at a time.
+
+ The external keyword argument denotes whether this lock should work across
+ multiple processes. This means that if two different workers both run a
+ a method decorated with @synchronized('mylock', external=True), only one
+ of them will execute at a time.
+ """
+
def wrap(f):
@functools.wraps(f)
def inner(*args, **kwargs):
- LOG.debug(_("Attempting to grab %(lock)s for method "
- "%(method)s..." % {"lock": name,
+ with _semaphores_semaphore:
+ if name not in _semaphores:
+ _semaphores[name] = semaphore.Semaphore()
+ sem = _semaphores[name]
+ LOG.debug(_('Attempting to grab semaphore "%(lock)s" for method '
+ '"%(method)s"...' % {"lock": name,
"method": f.__name__}))
- lock = lockfile.FileLock(os.path.join(FLAGS.lock_path,
- 'nova-%s.lock' % name))
- with lock:
- return f(*args, **kwargs)
+ with sem:
+ if external:
+ LOG.debug(_('Attempting to grab file lock "%(lock)s" for '
+ 'method "%(method)s"...' %
+ {"lock": name, "method": f.__name__}))
+ lock_file_path = os.path.join(FLAGS.lock_path,
+ 'nova-%s.lock' % name)
+ lock = lockfile.FileLock(lock_file_path)
+ else:
+ lock = _NoopContextManager()
+
+ with lock:
+ return f(*args, **kwargs)
+
return inner
return wrap