]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Improve callback registry devref documentation and usability
authorarmando-migliaccio <armamig@gmail.com>
Wed, 12 Aug 2015 01:16:30 +0000 (18:16 -0700)
committerarmando-migliaccio <armamig@gmail.com>
Wed, 12 Aug 2015 17:15:07 +0000 (10:15 -0700)
Latest developments have revealed that the registry can be misused
under certain circumstances, and that it can be harder to use by
projects that extend Neutron.

This patch improves the devref documentation so that developers know
what to expect.

Change-Id: I565b6a2f2a58bf22eae5b36f03c4fd24ba0774d2

doc/source/devref/callbacks.rst
neutron/callbacks/events.py
neutron/callbacks/manager.py
neutron/callbacks/resources.py
neutron/tests/unit/callbacks/test_manager.py

index 71c85f80edb34e3c38d51bd9150c2f7d82bc9ccf..ff6cfc77e8f9dab26a2f19ef09d979aaa49c9656 100644 (file)
@@ -300,6 +300,14 @@ The output is:
 FAQ
 ===
 
+Can I use the callbacks registry to subscribe and notify non-core resources and events?
+
+   Short answer is yes. The callbacks module defines literals for what are considered core Neutron
+   resources and events. However, the ability to subscribe/notify is not limited to these as you
+   can use your own defined resources and/or events. Just make sure you use string literals, as
+   typos are common, and the registry does not provide any runtime validation. Therefore, make
+   sure you test your code!
+
 What is the relationship between Callbacks and Taskflow?
 
    There is no overlap between Callbacks and Taskflow or mutual exclusion; as matter of fact they
@@ -315,6 +323,16 @@ Is there any ordering guarantee during notifications?
   notified. Priorities can be a future extension, if a use case arises that require enforced
   ordering.
 
+How is the the notifying object expected to interact with the subscribing objects?
+
+  The ``notify`` method implements a one-way communication paradigm: the notifier sends a message
+  without expecting a response back (in other words it fires and forget). However, due to the nature
+  of Python, the payload can be mutated by the subscribing objects, and this can lead to unexpected
+  behavior of your code, if you assume that this is the intentional design. Bear in mind, that
+  passing-by-value using deepcopy was not chosen for efficiency reasons. Having said that, if you
+  intend for the notifier object to expect a response, then the notifier itself would need to act
+  as a subscriber.
+
 Is the registry thread-safe?
 
   Short answer is no: it is not safe to make mutations while callbacks are being called (more
index 2abc57ce128899f6c4d88b989d3469a104b6fcd7..7dfd83d5e8e315d109dd5ae387c2bf6d33bbd367 100644 (file)
@@ -10,6 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+# String literals representing core events.
 BEFORE_CREATE = 'before_create'
 BEFORE_READ = 'before_read'
 BEFORE_UPDATE = 'before_update'
@@ -27,18 +28,3 @@ ABORT_DELETE = 'abort_delete'
 
 ABORT = 'abort_'
 BEFORE = 'before_'
-
-VALID = (
-    BEFORE_CREATE,
-    BEFORE_READ,
-    BEFORE_UPDATE,
-    BEFORE_DELETE,
-    AFTER_CREATE,
-    AFTER_READ,
-    AFTER_UPDATE,
-    AFTER_DELETE,
-    ABORT_CREATE,
-    ABORT_READ,
-    ABORT_UPDATE,
-    ABORT_DELETE,
-)
index 4927ff337f6b656300b4262388a8adbe2c2ae5af..c5b97e9af739c23d4bdad805d4cc96359de2512f 100644 (file)
@@ -17,7 +17,6 @@ from oslo_utils import reflection
 
 from neutron.callbacks import events
 from neutron.callbacks import exceptions
-from neutron.callbacks import resources
 from neutron.i18n import _LE
 
 LOG = logging.getLogger(__name__)
@@ -40,13 +39,15 @@ class CallbacksManager(object):
         """
         LOG.debug("Subscribe: %(callback)s %(resource)s %(event)s",
                   {'callback': callback, 'resource': resource, 'event': event})
-        if resource not in resources.VALID:
-            raise exceptions.Invalid(element='resource', value=resource)
-        if event not in events.VALID:
-            raise exceptions.Invalid(element='event', value=event)
 
         callback_id = _get_id(callback)
-        self._callbacks[resource][event][callback_id] = callback
+        try:
+            self._callbacks[resource][event][callback_id] = callback
+        except KeyError:
+            # Initialize the registry for unknown resources and/or events
+            # prior to enlisting the callback.
+            self._callbacks[resource][event] = {}
+            self._callbacks[resource][event][callback_id] = callback
         # We keep a copy of callbacks to speed the unsubscribe operation.
         if callback_id not in self._index:
             self._index[callback_id] = collections.defaultdict(set)
@@ -125,9 +126,6 @@ class CallbacksManager(object):
         """Brings the manager to a clean slate."""
         self._callbacks = collections.defaultdict(dict)
         self._index = collections.defaultdict(dict)
-        for resource in resources.VALID:
-            for event in events.VALID:
-                self._callbacks[resource][event] = collections.defaultdict()
 
     def _notify_loop(self, resource, event, trigger, **kwargs):
         """The notification loop."""
@@ -135,8 +133,9 @@ class CallbacksManager(object):
                   {'resource': resource, 'event': event})
 
         errors = []
+        callbacks = self._callbacks[resource].get(event, {}).items()
         # TODO(armax): consider using a GreenPile
-        for callback_id, callback in self._callbacks[resource][event].items():
+        for callback_id, callback in callbacks:
             try:
                 LOG.debug("Calling callback %s", callback_id)
                 callback(resource, event, trigger, **kwargs)
index d796faf49608487a45c4231fcaf23a380dc7e0ff..1544fe5a4b30460d898d8b2394f15bd157946c10 100644 (file)
@@ -10,6 +10,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+# String literals representing core resources.
 PORT = 'port'
 ROUTER = 'router'
 ROUTER_GATEWAY = 'router_gateway'
@@ -17,13 +18,3 @@ ROUTER_INTERFACE = 'router_interface'
 SECURITY_GROUP = 'security_group'
 SECURITY_GROUP_RULE = 'security_group_rule'
 SUBNET = 'subnet'
-
-VALID = (
-    PORT,
-    ROUTER,
-    ROUTER_GATEWAY,
-    ROUTER_INTERFACE,
-    SECURITY_GROUP,
-    SECURITY_GROUP_RULE,
-    SUBNET,
-)
index e4e64323d55bc8edab72885b9e90545de562278c..cdf32e020fc02f4604f414714d05da6a7a9aa684 100644 (file)
@@ -13,7 +13,6 @@
 #    under the License.
 
 import mock
-import testtools
 
 from neutron.callbacks import events
 from neutron.callbacks import exceptions
@@ -44,15 +43,6 @@ class CallBacksManagerTestCase(base.BaseTestCase):
         callback_1.counter = 0
         callback_2.counter = 0
 
-    def test_subscribe_invalid_resource_raise(self):
-        with testtools.ExpectedException(exceptions.Invalid):
-            self.manager.subscribe(mock.ANY, 'foo_resource', mock.ANY)
-
-    def test_subscribe_invalid_event_raise(self):
-        self.assertRaises(exceptions.Invalid,
-                  self.manager.subscribe,
-                  mock.ANY, mock.ANY, 'foo_event')
-
     def test_subscribe(self):
         self.manager.subscribe(
             callback_1, resources.PORT, events.BEFORE_CREATE)
@@ -60,6 +50,13 @@ class CallBacksManagerTestCase(base.BaseTestCase):
             self.manager._callbacks[resources.PORT][events.BEFORE_CREATE])
         self.assertIn(callback_id_1, self.manager._index)
 
+    def test_subscribe_unknown(self):
+        self.manager.subscribe(
+            callback_1, 'my_resource', 'my-event')
+        self.assertIsNotNone(
+            self.manager._callbacks['my_resource']['my-event'])
+        self.assertIn(callback_id_1, self.manager._index)
+
     def test_subscribe_is_idempotent(self):
         self.manager.subscribe(
             callback_1, resources.PORT, events.BEFORE_CREATE)