← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/longpoll-storm-events into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/longpoll-storm-events into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/launchpad/longpoll-storm-events/+merge/76215

This is a reduced version of the zope.event bridge I was working
on. This reduces the scope down to just Storm objects because we have
a reliable way of addressing them (i.e. to generate event_key) which
is to use the table name (assumed unique) and the primary key (unique
for the table).

I would *love* to instead do this using the web service definitions,
but there are problems with that, namely that there are multiple
versions of the web service API... and which one do we choose? Do
event emitters (which will often be outside of the web app) have to
emit messages for every API version? Unanswered questions... for now
event_key can remain opaque, but I would like to revisit this. For
one, using the web service names and links in events might make for
easier plumbing in the browser.

Also, the API versioning issue is also one reason why we can't send
the full objects out via the message queue. The other reason is
security; for now it's safer to say that something has changed, then
the browser must request the new version from the server.

It is *not* yet possible to land this branch because it will fall over
if a running RabbitMQ is not found, and there's no RabbitMQ in staging
or production. The messaging abstractions probably need to be softened
to work even if the message queue is unavailable.

-- 
https://code.launchpad.net/~allenap/launchpad/longpoll-storm-events/+merge/76215
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/longpoll-storm-events into lp:launchpad.
=== added file 'lib/lp/services/longpoll/adapters/storm.py'
--- lib/lp/services/longpoll/adapters/storm.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/longpoll/adapters/storm.py	2011-09-20 13:47:18 +0000
@@ -0,0 +1,70 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Long-poll life-cycle adapters."""
+
+from __future__ import absolute_import
+
+__metaclass__ = type
+__all__ = []
+
+from lazr.lifecycle.interfaces import (
+    IObjectCreatedEvent,
+    IObjectDeletedEvent,
+    IObjectModifiedEvent,
+    )
+from storm.base import Storm
+from storm.info import get_obj_info
+from zope.component import adapter
+from zope.interface import implements
+
+from lp.services.longpoll.adapters.event import (
+    generate_event_key,
+    LongPollEvent,
+    )
+from lp.services.longpoll.interfaces import ILongPollEvent
+
+
+class LongPollStormEvent(LongPollEvent):
+    """A `ILongPollEvent` for events of `Storm` objects.
+
+    This class knows how to construct a stable event key given a Storm object.
+    """
+
+    implements(ILongPollEvent)
+
+    @property
+    def event_key(self):
+        """See `ILongPollEvent`.
+
+        Constructs the key from the table name and primary key values of the
+        Storm model object.
+        """
+        cls_info = get_obj_info(self.source).cls_info
+        key_parts = [cls_info.table.name.lower()]
+        key_parts.extend(
+            primary_key_column.__get__(self.source)
+            for primary_key_column in cls_info.primary_key)
+        key_parts.append(self.event)
+        return generate_event_key(*key_parts)
+
+
+@adapter(Storm, IObjectCreatedEvent)
+def object_created(model_instance, object_event):
+    """Subscription handler for `Storm` creation events."""
+    event = LongPollStormEvent(model_instance, "created")
+    event.emit({})
+
+
+@adapter(Storm, IObjectDeletedEvent)
+def object_deleted(model_instance, object_event):
+    """Subscription handler for `Storm` deletion events."""
+    event = LongPollStormEvent(model_instance, "deleted")
+    event.emit({})
+
+
+@adapter(Storm, IObjectModifiedEvent)
+def object_modified(model_instance, object_event):
+    """Subscription handler for `Storm` modification events."""
+    event = LongPollStormEvent(model_instance, "modified")
+    event.emit({"edited_fields": sorted(object_event.edited_fields)})

=== added file 'lib/lp/services/longpoll/adapters/tests/test_storm.py'
--- lib/lp/services/longpoll/adapters/tests/test_storm.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/longpoll/adapters/tests/test_storm.py	2011-09-20 13:47:18 +0000
@@ -0,0 +1,74 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Long-poll event adapter tests."""
+
+__metaclass__ = type
+
+from lazr.lifecycle.event import (
+    ObjectCreatedEvent,
+    ObjectDeletedEvent,
+    ObjectModifiedEvent,
+    )
+from storm.base import Storm
+from storm.properties import Int
+from zope.event import notify
+
+from canonical.testing.layers import LaunchpadFunctionalLayer
+from lp.services.longpoll.testing import (
+    capture_longpoll_emissions,
+    LongPollEventRecord,
+    )
+from lp.testing import TestCase
+
+
+class FakeStormClass(Storm):
+
+    __storm_table__ = 'FakeTable'
+
+    id = Int(primary=True)
+
+
+class TestStormLifecycle(TestCase):
+
+    layer = LaunchpadFunctionalLayer
+
+    def test_storm_object_created(self):
+        storm_object = FakeStormClass()
+        storm_object.id = 1234
+        with capture_longpoll_emissions() as log:
+            notify(ObjectCreatedEvent(storm_object))
+        expected = [
+            LongPollEventRecord(
+                "longpoll.event.faketable.1234.created",
+                {"event_key": "longpoll.event.faketable.1234.created",
+                 "event_data": {}}),
+            ]
+        self.assertEqual(expected, log)
+
+    def test_storm_object_deleted(self):
+        storm_object = FakeStormClass()
+        storm_object.id = 1234
+        with capture_longpoll_emissions() as log:
+            notify(ObjectDeletedEvent(storm_object))
+        expected = [
+            LongPollEventRecord(
+                "longpoll.event.faketable.1234.deleted",
+                {"event_key": "longpoll.event.faketable.1234.deleted",
+                 "event_data": {}}),
+            ]
+        self.assertEqual(expected, log)
+
+    def test_storm_object_modified(self):
+        storm_object = FakeStormClass()
+        storm_object.id = 1234
+        with capture_longpoll_emissions() as log:
+            notify(ObjectModifiedEvent(
+                    storm_object, storm_object, ("itchy", "scratchy")))
+        expected = [
+            LongPollEventRecord(
+                "longpoll.event.faketable.1234.modified",
+                {"event_key": "longpoll.event.faketable.1234.modified",
+                 "event_data": {"edited_fields": ["itchy", "scratchy"]}}),
+            ]
+        self.assertEqual(expected, log)

=== modified file 'lib/lp/services/longpoll/configure.zcml'
--- lib/lp/services/longpoll/configure.zcml	2011-09-16 14:52:02 +0000
+++ lib/lp/services/longpoll/configure.zcml	2011-09-20 13:47:18 +0000
@@ -7,4 +7,7 @@
     xmlns:i18n="http://namespaces.zope.org/i18n";
     i18n_domain="launchpad">
     <adapter factory=".adapters.subscriber.LongPollSubscriber" />
+    <subscriber handler=".adapters.storm.object_created" />
+    <subscriber handler=".adapters.storm.object_deleted" />
+    <subscriber handler=".adapters.storm.object_modified" />
 </configure>

=== modified file 'lib/lp/services/longpoll/testing.py'
--- lib/lp/services/longpoll/testing.py	2011-09-19 12:58:06 +0000
+++ lib/lp/services/longpoll/testing.py	2011-09-20 13:47:18 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 __all__ = [
     "capture_longpoll_emissions",
+    "LongPollEventRecord",
     ]
 
 from collections import namedtuple