commit 3dbe3ddf257082ec3e5b9e3506aabe599ae6a8e3
Author: Michael Hanselmann <hansmi@google.com>
Date:   Fri May 6 16:43:38 2011 +0200

    SharedLock: Implement downgrade from exclusive to shared mode
    
    If a job needs to modify a resource and then wait for a result, it must
    acquire the resource lock in exclusive mode. In some cases it would be
    possible to only have a shared lock for waiting. Until now it was not
    possible to change a lock's mode once it'd been acquired. Releasing and
    re-acquiring might have been possible, but would require many more
    checks and can introduce new issues.
    
    With this patch a new method, named “downgrade”, is added to Ganeti's
    own SharedLock class. It can only be called when the lock is held in
    exclusive mode and changes it to shared. If there are any pending shared
    acquires on the same priority, they're moved to the front of the queue
    and notified (jumping ahead of exclusive acquires).
    
    In a lockset the internal lock will be downgraded if, and only if, all
    individual locks owned by the current thread are either released or
    acquired in shared mode.
    
    Unittests are provided.
    
    Signed-off-by: Michael Hanselmann <hansmi@google.com>
    Reviewed-by: Guido Trotter <ultrotter@google.com>

diff --git a/lib/locking.py b/lib/locking.py
index 5eb2c99..772c8f1 100644
--- a/lib/locking.py
+++ b/lib/locking.py
@@ -727,6 +727,48 @@ class SharedLock(object):
     finally:
       self.__lock.release()
 
+  def downgrade(self):
+    """Changes the lock mode from exclusive to shared.
+
+    Pending acquires in shared mode on the same priority will go ahead.
+
+    """
+    self.__lock.acquire()
+    try:
+      assert self.__is_owned(), "Lock must be owned"
+
+      if self.__is_exclusive():
+        # Do nothing if the lock is already acquired in shared mode
+        self.__exc = None
+        self.__do_acquire(1)
+
+        # Important: pending shared acquires should only jump ahead if there
+        # was a transition from exclusive to shared, otherwise an owner of a
+        # shared lock can keep calling this function to push incoming shared
+        # acquires
+        (priority, prioqueue) = self.__find_first_pending_queue()
+        if prioqueue:
+          # Is there a pending shared acquire on this priority?
+          cond = self.__pending_shared.pop(priority, None)
+          if cond:
+            assert cond.shared
+            assert cond in prioqueue
+
+            # Ensure shared acquire is on top of queue
+            if len(prioqueue) > 1:
+              prioqueue.remove(cond)
+              prioqueue.insert(0, cond)
+
+            # Notify
+            cond.notifyAll()
+
+      assert not self.__is_exclusive()
+      assert self.__is_sharer()
+
+      return True
+    finally:
+      self.__lock.release()
+
   def release(self):
     """Release a Shared Lock.
 
@@ -881,6 +923,21 @@ class LockSet:
     """
     return "%s/%s" % (self.name, mname)
 
+  def _get_lock(self):
+    """Returns the lockset-internal lock.
+
+    """
+    return self.__lock
+
+  def _get_lockdict(self):
+    """Returns the lockset-internal lock dictionary.
+
+    Accessing this structure is only safe in single-thread usage or when the
+    lockset-internal lock is held.
+
+    """
+    return self.__lockdict
+
   def _is_owned(self):
     """Is the current thread a current level owner?"""
     return threading.currentThread() in self.__owners
@@ -1122,6 +1179,42 @@ class LockSet:
 
     return acquired
 
+  def downgrade(self, names=None):
+    """Downgrade a set of resource locks from exclusive to shared mode.
+
+    The locks must have been acquired in exclusive mode.
+
+    """
+    assert self._is_owned(), ("downgrade on lockset %s while not owning any"
+                              " lock" % self.name)
+
+    # Support passing in a single resource to downgrade rather than many
+    if isinstance(names, basestring):
+      names = [names]
+
+    owned = self._list_owned()
+
+    if names is None:
+      names = owned
+    else:
+      names = set(names)
+      assert owned.issuperset(names), \
+        ("downgrade() on unheld resources %s (set %s)" %
+         (names.difference(owned), self.name))
+
+    for lockname in names:
+      self.__lockdict[lockname].downgrade()
+
+    # Do we own the lockset in exclusive mode?
+    if self.__lock._is_owned(shared=0):
+      # Have all locks been downgraded?
+      if not compat.any(lock._is_owned(shared=0)
+                        for lock in self.__lockdict.values()):
+        self.__lock.downgrade()
+        assert self.__lock._is_owned(shared=1)
+
+    return True
+
   def release(self, names=None):
     """Release a set of resource locks, at the same level.
 
@@ -1458,6 +1551,22 @@ class GanetiLockManager:
     return self.__keyring[level].acquire(names, shared=shared, timeout=timeout,
                                          priority=priority)
 
+  def downgrade(self, level, names=None):
+    """Downgrade a set of resource locks from exclusive to shared mode.
+
+    You must have acquired the locks in exclusive mode.
+
+    @type level: member of locking.LEVELS
+    @param level: the level at which the locks shall be downgraded
+    @type names: list of strings, or None
+    @param names: the names of the locks which shall be downgraded
+        (defaults to all the locks acquired at the level)
+
+    """
+    assert level in LEVELS, "Invalid locking level %s" % level
+
+    return self.__keyring[level].downgrade(names=names)
+
   def release(self, level, names=None):
     """Release a set of resource locks, at the same level.
 
