From ebce40f489c871a3a0a2a2000f9494ddb70ddf6c Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Wed, 8 Apr 2015 10:51:31 -0500
Subject: [PATCH 10/51] Make the main class hierarchies thread-safe.

This adds a global reentrant lock and a metaclass. The metaclass wraps
every method call with a decorator that acquires the global lock prior
to calling the method.
---
 blivet/actionlist.py            | 10 ++++++-
 blivet/deviceaction.py          |  4 ++-
 blivet/devices/container.py     |  3 +-
 blivet/devices/device.py        |  3 ++
 blivet/devices/lvm.py           |  3 +-
 blivet/devices/raid.py          |  5 ++--
 blivet/devicetree.py            |  4 +++
 blivet/flags.py                 |  1 +
 blivet/formats/__init__.py      |  3 ++
 blivet/populator.py             |  3 ++
 blivet/threads.py               | 66 +++++++++++++++++++++++++++++++++++++++++
 tests/formats_test/init_test.py |  4 ++-
 12 files changed, 101 insertions(+), 8 deletions(-)
 create mode 100644 blivet/threads.py

diff --git a/blivet/actionlist.py b/blivet/actionlist.py
index eed5c2c..e4fd84f 100644
--- a/blivet/actionlist.py
+++ b/blivet/actionlist.py
@@ -21,6 +21,7 @@
 #
 
 import copy
+from six import add_metaclass
 
 from .deviceaction import ActionCreateDevice, ActionDestroyDevice
 from .deviceaction import action_type_from_string, action_object_from_string
@@ -29,11 +30,15 @@ from .devices import PartitionDevice
 from .errors import DiskLabelCommitError, StorageError
 from .flags import flags
 from . import tsort
+from .threads import blivet_lock, SynchronizedMeta
 
 import logging
 log = logging.getLogger("blivet")
 
+@add_metaclass(SynchronizedMeta)
 class ActionList(object):
+    _unsynchronized_methods = ['process']
+
     def __init__(self):
         self._actions = []
         self._completed_actions = []
@@ -279,7 +284,10 @@ class ActionList(object):
 
         for action in self._actions[:]:
             log.info("executing action: %s", action)
-            if not dryRun:
+            if dryRun:
+                continue
+
+            with blivet_lock:
                 try:
                     action.execute(callbacks)
                 except DiskLabelCommitError:
diff --git a/blivet/deviceaction.py b/blivet/deviceaction.py
index 4d10765..a2c2cda 100644
--- a/blivet/deviceaction.py
+++ b/blivet/deviceaction.py
@@ -20,9 +20,9 @@
 # Red Hat Author(s): Dave Lehman <dlehman@redhat.com>
 #
 
+from six import add_metaclass
 
 from . import util
-
 from . import udev
 from .util import get_current_entropy
 from .devices import StorageDevice
@@ -34,6 +34,7 @@ from .callbacks import CreateFormatPreData, CreateFormatPostData
 from .callbacks import ResizeFormatPreData, ResizeFormatPostData
 from .callbacks import WaitForEntropyData
 from .size import Size
+from .threads import SynchronizedMeta
 
 import logging
 log = logging.getLogger("blivet")
@@ -96,6 +97,7 @@ def resize_type_from_string(type_string):
         if v.lower() == type_string.lower():
             return k
 
+@add_metaclass(SynchronizedMeta)
 class DeviceAction(util.ObjectID):
     """ An action that will be carried out in the future on a Device.
 
diff --git a/blivet/devices/container.py b/blivet/devices/container.py
index 0de6ca7..cc75880 100644
--- a/blivet/devices/container.py
+++ b/blivet/devices/container.py
@@ -26,13 +26,14 @@ from six import add_metaclass
 from .. import errors
 from ..storage_log import log_method_call
 from ..formats import get_device_format_class
+from ..threads import SynchronizedABCMeta
 
 import logging
 log = logging.getLogger("blivet")
 
 from .storage import StorageDevice
 
-@add_metaclass(abc.ABCMeta)
+@add_metaclass(SynchronizedABCMeta)
 class ContainerDevice(StorageDevice):
     """ A device that aggregates a set of member devices.
 
diff --git a/blivet/devices/device.py b/blivet/devices/device.py
index 247f792..321d5d3 100644
--- a/blivet/devices/device.py
+++ b/blivet/devices/device.py
@@ -21,15 +21,18 @@
 #
 
 import pprint
+from six import add_metaclass
 
 from .. import util
 from ..storage_log import log_method_call
+from ..threads import SynchronizedMeta
 
 import logging
 log = logging.getLogger("blivet")
 
 from .lib import ParentList
 
+@add_metaclass(SynchronizedMeta)
 class Device(util.ObjectID):
     """ A generic device.
 
diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py
index d7526e4..bc3b8d1 100644
--- a/blivet/devices/lvm.py
+++ b/blivet/devices/lvm.py
@@ -35,6 +35,7 @@ from ..formats import getFormat
 from ..storage_log import log_method_call
 from .. import udev
 from ..size import Size, KiB, MiB, ROUND_UP, ROUND_DOWN
+from ..threads import SynchronizedABCMeta
 
 import logging
 log = logging.getLogger("blivet")
@@ -793,7 +794,7 @@ class LVMLogicalVolumeDevice(DMDevice):
 
         return True
 
-@add_metaclass(abc.ABCMeta)
+@add_metaclass(SynchronizedABCMeta)
 class LVMSnapShotBase(object):
     """ Abstract base class for lvm snapshots
 
diff --git a/blivet/devices/raid.py b/blivet/devices/raid.py
index 60fb5a2..8638ecf 100644
--- a/blivet/devices/raid.py
+++ b/blivet/devices/raid.py
@@ -19,16 +19,15 @@
 # Red Hat Author(s): Anne Mulhern <amulhern@redhat.com>
 #
 
-import abc
-
 from six import add_metaclass
 
 from .. import errors
 from ..i18n import _, P_
+from ..threads import SynchronizedABCMeta
 
 from .storage import StorageDevice
 
-@add_metaclass(abc.ABCMeta)
+@add_metaclass(SynchronizedABCMeta)
 class RaidDevice(StorageDevice):
     """ Metaclass for devices that support RAID in some form. """
 
diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index 2688078..dbb707c 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -22,6 +22,7 @@
 
 import os
 import re
+from six import add_metaclass
 
 from gi.repository import BlockDev as blockdev
 
@@ -38,12 +39,14 @@ from . import util
 from .flags import flags
 from .populator import Populator
 from .storage_log import log_method_call, log_method_return
+from .threads import SynchronizedMeta
 
 import logging
 log = logging.getLogger("blivet")
 
 _LVM_DEVICE_CLASSES = (LVMLogicalVolumeDevice, LVMVolumeGroupDevice)
 
+@add_metaclass(SynchronizedMeta)
 class DeviceTree(object):
     """ A quasi-tree that represents the devices in the system.
 
@@ -63,6 +66,7 @@ class DeviceTree(object):
         :class:`~.deviceaction.DeviceAction` instances can only be registered
         for leaf devices, except for resize actions.
     """
+    _unsynchronized_methods = ["processActions"]
 
     def __init__(self, conf=None, passphrase=None, luksDict=None,
                  iscsi=None, dasd=None):
diff --git a/blivet/flags.py b/blivet/flags.py
index 0ff1c96..2e5e47e 100644
--- a/blivet/flags.py
+++ b/blivet/flags.py
@@ -68,6 +68,7 @@ class Flags(object):
 
         self.update_from_boot_cmdline()
         self.allow_imperfect_devices = True
+        self.debug_threads = False
 
     def get_boot_cmdline(self):
         buf = open("/proc/cmdline").read().strip()
diff --git a/blivet/formats/__init__.py b/blivet/formats/__init__.py
index 25beddf..86ba415 100644
--- a/blivet/formats/__init__.py
+++ b/blivet/formats/__init__.py
@@ -24,6 +24,7 @@
 import os
 import importlib
 from gi.repository import BlockDev as blockdev
+from six import add_metaclass
 
 from ..util import notify_kernel
 from ..util import get_sysfs_path_by_name
@@ -33,6 +34,7 @@ from ..storage_log import log_method_call
 from ..errors import DeviceFormatError, FormatCreateError, FormatDestroyError, FormatSetupError
 from ..i18n import N_
 from ..size import Size
+from ..threads import SynchronizedMeta
 
 import logging
 log = logging.getLogger("blivet")
@@ -137,6 +139,7 @@ def get_device_format_class(fmt_type):
 
     return fmt
 
+@add_metaclass(SynchronizedMeta)
 class DeviceFormat(ObjectID):
     """ Generic device format.
 
diff --git a/blivet/populator.py b/blivet/populator.py
index 5d9ae39..61b356f 100644
--- a/blivet/populator.py
+++ b/blivet/populator.py
@@ -26,6 +26,7 @@ import shutil
 import pprint
 import copy
 import parted
+from six import add_metaclass
 
 from gi.repository import BlockDev as blockdev
 
@@ -50,6 +51,7 @@ from .flags import flags
 from .storage_log import log_exception_info, log_method_call
 from .i18n import _
 from .size import Size
+from .threads import SynchronizedMeta
 
 import logging
 log = logging.getLogger("blivet")
@@ -66,6 +68,7 @@ def parted_exn_handler(exn_type, exn_options, exn_msg):
         ret = parted.EXCEPTION_RESOLVE_YES
     return ret
 
+@add_metaclass(SynchronizedMeta)
 class Populator(object):
     def __init__(self, devicetree=None, conf=None, passphrase=None,
                  luksDict=None, iscsi=None, dasd=None):
diff --git a/blivet/threads.py b/blivet/threads.py
new file mode 100644
index 0000000..161d390
--- /dev/null
+++ b/blivet/threads.py
@@ -0,0 +1,66 @@
+# threads.py
+# Utilities related to multithreading.
+#
+# Copyright (C) 2014,2015  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU Lesser General Public License v.2, or (at your option) any later
+# version. This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY expressed or implied, including the implied
+# warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
+# the GNU Lesser General Public License for more details.  You should have
+# received a copy of the GNU Lesser General Public License along with this
+# program; if not, write to the Free Software Foundation, Inc., 51 Franklin
+# Street, Fifth Floor, Boston, MA 02110-1301, USA.  Any Red Hat trademarks
+# that are incorporated in the source code or documentation are not subject
+# to the GNU Lesser General Public License and may only be used or
+# replicated with the express permission of Red Hat, Inc.
+#
+# Red Hat Author(s): David Lehman <dlehman@redhat.com>
+#
+
+from threading import RLock
+from functools import wraps
+from types import FunctionType
+from abc import ABCMeta
+
+from .flags import flags
+
+blivet_lock = RLock(verbose=flags.debug_threads)
+
+def exclusive(m):
+    """ Run a bound method after aqcuiring the instance's lock. """
+    @wraps(m)
+    def run_with_lock(*args, **kwargs):
+        # When the decorator is applied during creation of the method's class
+        # instance we have a function object -- not a method. As a result, we
+        # cannot use m.__self__ and instead must rely on the fact that the
+        # instance is always passed as the first argument to the method.
+        with blivet_lock:
+            return m(*args, **kwargs)
+
+    return run_with_lock
+
+class SynchronizedMeta(type):
+    """ Metaclass that wraps all methods with the exclusive decorator.
+
+        To prevent specific methods from being wrapped, add the method name(s)
+        to a class attribute called _unsynchronized_methods (list of str).
+    """
+    def __new__(cls, name, bases, dct):
+        wrapped = {}
+        blacklist = dct.get('_unsynchronized_methods', [])
+        for n in dct:
+            obj = dct[n]
+            # Do not decorate class or static methods.
+            if type(dct[n]) is FunctionType:
+                if n not in blacklist:
+                    obj = exclusive(obj)
+
+            wrapped[n] = obj
+
+        return super(SynchronizedMeta, cls).__new__(cls, name, bases, wrapped)
+
+class SynchronizedABCMeta(SynchronizedMeta, ABCMeta):
+    pass
diff --git a/tests/formats_test/init_test.py b/tests/formats_test/init_test.py
index 6fc11c6..b609165 100644
--- a/tests/formats_test/init_test.py
+++ b/tests/formats_test/init_test.py
@@ -29,10 +29,12 @@ class FormatsTestCase(unittest.TestCase):
             self.assertIs(formats.getFormat(name).__class__,
                formats.DeviceFormat if format_pairs[name] is None else format_pairs[name])
         ## Consecutively constructed DeviceFormat objects have consecutive ids
+        ## that increment by two because each instance contains an instance of
+        ## StorageEventSychronizer, which also inherits from ObjectID.
         names = [key for key in format_pairs.keys() if format_pairs[key] is not None]
         objs = [formats.getFormat(name) for name in names]
         ids = [obj.id for obj in objs]
-        self.assertEqual(ids, list(range(ids[0], ids[0] + len(ids))))
+        self.assertEqual(ids, list(range(ids[0], ids[0] + 2*len(ids), 2)))
 
         ## Copy or deepcopy should preserve the id
         self.assertEqual(ids, [copy.copy(obj).id for obj in objs])
-- 
2.1.0

