Skip to content

Commit

Permalink
Workaround issue with greenthreads and lockfiles
Browse files Browse the repository at this point in the history
 * Adds a GreenLockFile that always works in greenthreads
 * Adds test to verify that regular Lockfile is broken
 * Adds test to verify that GreenLockfile works
 * Adds note about limitation of external locks
 * Adds test showing limitation of nested locks
 * Fixes bug 956313

Change-Id: I11cd1206611aa4862dadd2fcc077c4c2e0f798f6
  • Loading branch information
vishvananda committed Mar 16, 2012
1 parent 1ecf2c5 commit eb42e7f
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 1 deletion.
16 changes: 16 additions & 0 deletions nova/tests/test_misc.py
Expand Up @@ -22,6 +22,7 @@

from eventlet import greenpool
from eventlet import greenthread
import lockfile

from nova import exception
from nova import test
Expand Down Expand Up @@ -134,6 +135,21 @@ def f(id):
self.assertEqual(saved_sem_num, len(utils._semaphores),
"Semaphore leak detected")

def test_nested_external_fails(self):
"""We can not nest external syncs"""

@utils.synchronized('testlock1', external=True)
def outer_lock():

@utils.synchronized('testlock2', external=True)
def inner_lock():
pass
inner_lock()
try:
self.assertRaises(lockfile.NotMyLock, outer_lock)
finally:
utils.cleanup_file_locks()

def test_synchronized_externally(self):
"""We can lock across multiple processes"""
rpipe1, wpipe1 = os.pipe()
Expand Down
39 changes: 39 additions & 0 deletions nova/tests/test_utils.py
Expand Up @@ -24,7 +24,10 @@
import StringIO
import tempfile

import eventlet
from eventlet import greenpool
import iso8601
import lockfile
import mox

import nova
Expand Down Expand Up @@ -832,6 +835,42 @@ def test_west_normalize(self):
self._instaneous(normed, 2012, 2, 13, 23, 53, 07, 0)


class TestGreenLocks(test.TestCase):
def test_concurrent_green_lock_succeeds(self):
"""Verify spawn_n greenthreads with two locks run concurrently.
This succeeds with spawn but fails with spawn_n because lockfile
gets the same thread id for both spawn_n threads. Our workaround
of using the GreenLockFile will work even if the issue is fixed.
"""
self.completed = False
with utils.tempdir() as tmpdir:

def locka(wait):
a = utils.GreenLockFile(os.path.join(tmpdir, 'a'))
a.acquire()
wait.wait()
a.release()
self.completed = True

def lockb(wait):
b = utils.GreenLockFile(os.path.join(tmpdir, 'b'))
b.acquire()
wait.wait()
b.release()

wait1 = eventlet.event.Event()
wait2 = eventlet.event.Event()
pool = greenpool.GreenPool()
pool.spawn_n(locka, wait1)
pool.spawn_n(lockb, wait2)
wait2.send()
eventlet.sleep(0)
wait1.send()
pool.waitall()
self.assertTrue(self.completed)


class TestLockCleanup(test.TestCase):
"""unit tests for utils.cleanup_file_locks()"""

Expand Down
44 changes: 43 additions & 1 deletion nova/utils.py
Expand Up @@ -36,12 +36,14 @@
import struct
import sys
import tempfile
import threading
import time
import types
import uuid
import warnings
from xml.sax import saxutils

from eventlet import corolocal
from eventlet import event
from eventlet import greenthread
from eventlet import semaphore
Expand Down Expand Up @@ -838,6 +840,33 @@ def loads(s):
anyjson.force_implementation("nova.utils")


class GreenLockFile(lockfile.FileLock):
"""Implementation of lockfile that allows for a lock per greenthread.
Simply implements lockfile:LockBase init with an addiontall suffix
on the unique name of the greenthread identifier
"""
def __init__(self, path, threaded=True):
self.path = path
self.lock_file = os.path.abspath(path) + ".lock"
self.hostname = socket.gethostname()
self.pid = os.getpid()
if threaded:
t = threading.current_thread()
# Thread objects in Python 2.4 and earlier do not have ident
# attrs. Worm around that.
ident = getattr(t, "ident", hash(t))
gident = corolocal.get_ident()
self.tname = "-%x-%x" % (ident & 0xffffffff, gident & 0xffffffff)
else:
self.tname = ""
dirname = os.path.dirname(self.lock_file)
self.unique_name = os.path.join(dirname,
"%s%s.%s" % (self.hostname,
self.tname,
self.pid))


_semaphores = {}


Expand Down Expand Up @@ -869,6 +898,19 @@ def bar(self, *args):
a method decorated with @synchronized('mylock', external=True), only one
of them will execute at a time.
Important limitation: you can only have one external lock running per
thread at a time. For example the following will fail:
@utils.synchronized('testlock1', external=True)
def outer_lock():
@utils.synchronized('testlock2', external=True)
def inner_lock():
pass
inner_lock()
outer_lock()
"""

def wrap(f):
Expand All @@ -893,7 +935,7 @@ def inner(*args, **kwargs):
{'lock': name, 'method': f.__name__})
lock_file_path = os.path.join(FLAGS.lock_path,
'nova-%s' % name)
lock = lockfile.FileLock(lock_file_path)
lock = GreenLockFile(lock_file_path)
with lock:
LOG.debug(_('Got file lock "%(lock)s" for '
'method "%(method)s"...') %
Expand Down

0 comments on commit eb42e7f

Please sign in to comment.