← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/launchpad/lp-app-longpoll-js into lp:launchpad

 

Raphaël Victor Badin has proposed merging lp:~rvb/launchpad/lp-app-longpoll-js into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/launchpad/lp-app-longpoll-js/+merge/66936

This branches adds the js side for the new lp.app.longpoll package (see lp:~allenap/launchpad/lp-app/longpoll/). The whole machinery is present on every page but it really kicks in (i.e. starts a long polling request) only if LP.cache.longpoll is populated.

Every page view that has registered into the event machinery on the server side will start a long polling request on the client side.

    subscribe(an_object, "event_name")

When the event is emitted on the server side:

    # Server side.
    emit(an_object, "event_name", payload)

... the very same event will be fired as a Javascript event to be dealt with by the page's javascript code:

    // Client side.
    Y.on("event_name", function(payload) {
        // Do something.
    });

Additional events are provided as part of the longpoll js machinery:
'lp.app.longpoll.start' will be triggered when/if the long polling request is started.
'lp.app.longpoll.failure' will be triggered each time the long polling request fails to connect or to parse the returned data.

Note that the long polling request will be restarted after one event has been consumed so the number of events is not limited.

To avoid initiating requests too quickly if the server is down or has capacity problems, every failed request will wait for 1 second before attempting to reconnect. After 5 failed attempts, the long polling machinery will wait for 3 minutes before trying to reconnect again.
-- 
https://code.launchpad.net/~rvb/launchpad/lp-app-longpoll-js/+merge/66936
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/launchpad/lp-app-longpoll-js into lp:launchpad.
=== added directory 'lib/lp/app/longpoll'
=== added directory 'lib/lp/app/longpoll/javascript'
=== added file 'lib/lp/app/longpoll/javascript/longpoll.js'
--- lib/lp/app/longpoll/javascript/longpoll.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/javascript/longpoll.js	2011-07-05 17:19:40 +0000
@@ -0,0 +1,151 @@
+/* Copyright 2009 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * The Launchpad Longpoll module provides the functionnality to deal
+ * with longpolling on the JavaScript side.
+ *
+ * @module longpoll
+ * @requires event, LP
+ */
+YUI.add('lp.app.longpoll', function(Y) {
+
+var namespace = Y.namespace('lp.app.longpoll');
+
+namespace.longpoll_start_event = 'lp.app.longpoll.start';
+namespace.longpoll_fail_event = 'lp.app.longpoll.failure';
+
+namespace._manager = null;
+
+// After MAX_FAILED_ATTEMPTS failed connections (real failed
+// connections  or connection with invalid returns) separate
+// by SHORT_DELAY (millisec), wait LONG_DELAY (millisec) before
+// trying again to connect.
+namespace.MAX_FAILED_ATTEMPTS = 5;
+namespace.SHORT_DELAY = 1000;
+namespace.LONG_DELAY = 3*60*1000;
+
+function LongPollManager() {
+    this.started = false;
+    this._failed_attempts = 0;
+    this._repoll = true; // Used in tests.
+}
+
+/**
+ *
+ * A Long Poll Manager creates and manages a long polling connexion
+ * to the server to fetch events. This class is not directly used
+ * but manager through 'setupLongPollManager' which creates and
+ * initialises a singleton LongPollManager.
+ *
+ * @class LongPollManager
+ */
+namespace.LongPollManager = LongPollManager;
+
+LongPollManager.prototype.initialize = function(queue_name, uri) {
+    this._sequence = 0;
+    this.queue_name = queue_name;
+    this.uri = uri;
+};
+
+LongPollManager.prototype.io = function (uri, config) {
+    Y.io(uri, config);
+};
+
+LongPollManager.prototype.success_poll = function (id, response) {
+    try {
+        var data = Y.JSON.parse(response.responseText);
+        var event_key = data.event_key;
+        var event_data = data.event_data;
+        Y.fire(event_key, event_data);
+        return true;
+    }
+    catch (e) {
+        Y.fire(namespace.longpoll_fail_event, e);
+        return false;
+    }
+};
+
+LongPollManager.prototype.failure_poll = function () {
+    Y.fire(namespace.longpoll_fail_event);
+};
+
+LongPollManager.prototype._poll_delay = function() {
+    this._failed_attempts = this._failed_attempts + 1;
+    if (this._failed_attempts >= namespace.MAX_FAILED_ATTEMPTS) {
+        this._failed_attempts = 0;
+        return namespace.LONG_DELAY;
+    }
+    else {
+        return namespace.SHORT_DELAY;
+    }
+};
+
+/**
+ * Relaunch a connection to the server after a successful or
+ * a failed connection.
+ *
+ * @method repoll
+ * @param {Boolean} failed: whether or not the previous connection
+ *     has failed.
+ */
+LongPollManager.prototype.repoll = function(failed) {
+    if (!failed) {
+        this._failed_attempts = 0;
+        if (this._repoll) {
+            this.poll();
+        }
+    }
+    else {
+        var delay = this._poll_delay();
+        if (this._repoll) {
+            Y.later(delay, this, this.poll);
+        }
+    }
+};
+
+LongPollManager.prototype.poll = function() {
+    var closure = this;
+    var config = {
+        method: "GET",
+        sync: false,
+        on: {
+            failure: function() {
+                closure.failure_poll();
+                closure.repoll(true);
+            },
+            success: function(id, response) {
+                var res = closure.success_poll(id, response);
+                closure.repoll(res);
+            }
+        }
+    };
+    this._sequence = this._sequence + 1;
+    var queue_uri = this.uri +
+        "?uuid=" + this.queue_name +
+        "&sequence=" + this._sequence;
+    if (!this.started) {
+        Y.fire(namespace.longpoll_start_event);
+        this.started = true;
+    }
+    this.io(queue_uri, config);
+};
+
+namespace.getLongPollManager = function() {
+    if (!Y.Lang.isValue(namespace._manager)) {
+        namespace._manager = new namespace.LongPollManager();
+    }
+    return namespace._manager;
+};
+
+namespace.setupLongPollManager = function() {
+    if (Y.Lang.isValue(LP.cache.longpoll)) {
+        var queue_name = LP.cache.longpoll.key;
+        var uri = LP.cache.longpoll.uri;
+        var longpollmanager = namespace.getLongPollManager();
+        longpollmanager.initialize(queue_name, uri);
+        longpollmanager.poll();
+        return longpollmanager;
+    }
+};
+
+}, "0.1", {"requires":["event", "plugin", "lang", "json", "io"]});

=== added directory 'lib/lp/app/longpoll/javascript/tests'
=== added file 'lib/lp/app/longpoll/javascript/tests/test_longpoll.html'
--- lib/lp/app/longpoll/javascript/tests/test_longpoll.html	1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/javascript/tests/test_longpoll.html	2011-07-05 17:19:40 +0000
@@ -0,0 +1,30 @@
+<!DOCTYPE
+   HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+   "http://www.w3.org/TR/html4/strict.dtd";>
+<html>
+  <head>
+    <title>Launchpad Longpoll</title>
+    <!-- YUI 3.0 Setup -->
+    <script type="text/javascript"
+            src="../../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
+    <script type="text/javascript"
+            src="../../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
+    <link rel="stylesheet"
+          href="../../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
+    <link rel="stylesheet"
+          href="../../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
+    <link rel="stylesheet"
+          href="../../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
+    <link rel="stylesheet"
+          href="../../../../../canonical/launchpad/javascript/test.css" />
+    <!-- Required modules -->
+    <script type="text/javascript" src="../../../../app/javascript/client.js"></script>
+    <!-- The module under test -->
+    <script type="text/javascript" src="../longpoll.js"></script>
+    <!-- The test suite -->
+    <script type="text/javascript" src="test_longpoll.js"></script>
+  </head>
+  <body class="yui3-skin-sam">
+    <div id="log"></div>
+  </body>
+</html>

=== added file 'lib/lp/app/longpoll/javascript/tests/test_longpoll.js'
--- lib/lp/app/longpoll/javascript/tests/test_longpoll.js	1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/javascript/tests/test_longpoll.js	2011-07-05 17:19:40 +0000
@@ -0,0 +1,266 @@
+/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
+
+YUI({
+    base: '../../../../../canonical/launchpad/icing/yui/',
+    filter: 'raw', combine: false, fetchCSS: false
+    }).use(
+        'test', 'console', 'node-event-simulate', 'json',
+        'lp.app.longpoll',
+        function(Y) {
+
+    var Assert = Y.Assert;
+    var ArrayAssert = Y.ArrayAssert;
+
+    var suite = new Y.Test.Suite("longpoll Tests");
+    var longpoll = Y.lp.app.longpoll;
+
+    var attrgetter = function(name) {
+        return function(thing) {
+            return thing[name];
+        };
+    };
+
+    var attrselect = function(name) {
+        return function(things) {
+            return Y.Array(things).map(attrgetter(name));
+        };
+    };
+    var testLongPollSingleton = {
+        name: 'TestLongPollSingleton',
+        tearDown: function() {
+            // Cleanup the singleton;
+            longpoll._manager = null;
+        },
+
+        testGetSingletonLongPollManager: function() {
+            Assert.isNull(longpoll._manager);
+            var manager = longpoll.getLongPollManager();
+            Assert.isNotNull(longpoll._manager);
+            var manager2 = longpoll.getLongPollManager();
+            Assert.areSame(manager, manager2);
+        },
+
+        testInitLongPollManagerNoLongPoll: function() {
+            // if LP.cache.longpoll.key is undefined: no longpoll manager
+            // is created by setupLongPollManager.
+            window.LP = {
+                links: {},
+                cache: {}
+            };
+
+            longpoll.setupLongPollManager(true);
+            Assert.isNull(longpoll._manager);
+        },
+
+        testInitLongPollManagerLongPoll: function() {
+            window.LP = {
+                links: {},
+                cache: {}
+            };
+
+            LP.cache.longpoll = {
+                key: 'key',
+                uri: '/+longpoll/'
+            };
+            longpoll.setupLongPollManager(true);
+            Assert.isNotNull(longpoll._manager);
+        }
+    };
+
+    suite.add(new Y.Test.Case(testLongPollSingleton));
+
+    var testLongPoll = {
+        name: 'TestLongPoll',
+
+        setUp: function() {
+            var manager = longpoll.getLongPollManager();
+            manager._repoll = false;
+            this.createBaseLP();
+        },
+
+        tearDown: function() {
+            // Cleanup the singleton;
+            longpoll._manager = null;
+        },
+
+        createBaseLP:function() {
+            window.LP = {
+                links: {},
+                cache: {}
+            };
+        },
+
+        setupLPCache: function() {
+            LP.cache.longpoll = {
+                key: 'key',
+                uri: '/+longpoll/'
+            };
+        },
+
+       setupLongPoll: function(nb_calls) {
+            this.setupLPCache();
+            return longpoll.setupLongPollManager(true);
+        },
+
+        testInitLongPollManagerQueueName: function() {
+            var manager = this.setupLongPoll();
+            Assert.areEqual(LP.cache.longpoll.key, manager.queue_name);
+            Assert.areEqual(LP.cache.longpoll.uri, manager.uri);
+            Assert.isFalse(Y.Lang.isValue(manager.nb_calls));
+        },
+
+        testPollStarted: function() {
+            var fired = false;
+            Y.on(longpoll.longpoll_start_event, function() {
+                fired = true;
+            });
+            var manager = this.setupLongPoll();
+            Assert.isTrue(fired, "Start event not fired.");
+        },
+
+        xtestPollFailure: function() {
+            var fired = false;
+            Y.on(longpoll.longpoll_fail_event, function() {
+                fired = true;
+            });
+            // Monkeypatch io to simulate failure.
+            var manager = longpoll.getLongPollManager();
+            manager.io = function(uri, config) {
+                config.on.failure();
+            };
+            this.setupLongPoll();
+            Assert.isTrue(fired, "Failure event not fired.");
+        },
+
+        testSuccessPollInvalidData: function() {
+            var manager = longpoll.getLongPollManager();
+            var custom_response = "{{";
+            var response = {
+                responseText: custom_response
+            };
+            var res = manager.success_poll("2", response);
+            Assert.isFalse(res);
+        },
+
+        testSuccessPollMalformedData: function() {
+            var manager = longpoll.getLongPollManager();
+            var response = {
+                responseText: '{ "event_data": "6" }'
+            };
+            var res = manager.success_poll("2", response);
+            Assert.isFalse(res);
+         },
+
+         testSuccessPollWellformedData: function() {
+            var manager = longpoll.getLongPollManager();
+            var response = {
+                responseText: '{ "event_key": "4", "event_data": "6"}'
+            };
+            var res = manager.success_poll("2", response);
+            Assert.isTrue(res);
+        },
+
+        testPollDelay: function() {
+            var manager = longpoll.getLongPollManager();
+            // Monkeypatch io to simulate failure.
+            manager.io = function(uri, config) {
+                config.on.failure();
+            };
+            Assert.areEqual(0, manager._failed_attempts);
+            this.setupLongPoll();
+            Assert.areEqual(1, manager._failed_attempts);
+            var i;
+            for (i=0; i<longpoll.MAX_FAILED_ATTEMPTS-1; i++) {
+                Assert.areEqual(i+1, manager._failed_attempts);
+                manager.poll(); // Fail.
+            }
+            // _failed_attempts has been reset.
+            Assert.areEqual(0, manager._failed_attempts);
+        },
+
+        testPollUriSequence: function() {
+            // Each new polling increses the sequence parameter:
+            // /+longpoll/?uuid=key&sequence=1
+            // /+longpoll/?uuid=key&sequence=2
+            // /+longpoll/?uuid=key&sequence=3
+            // ..
+            var count = 0;
+            // Monkeypatch io to simulate failure.
+            var manager = longpoll.getLongPollManager();
+            manager.io = function(uri, config) {
+                Assert.areEqual(
+                    '/+longpoll/?uuid=key&sequence=' + (count+1),
+                    uri);
+                count = count + 1;
+                var response = {
+                   responseText: '{"i":2}'
+                };
+                config.on.success(2, response);
+            };
+            this.setupLongPoll();
+            Assert.isTrue(count === 1, "Uri not requested.");
+        },
+
+        testPollPayLoadBad: function() {
+            // If a non valid response is returned, longpoll_fail_event
+            // is fired.
+            var fired = false;
+            Y.on(longpoll.longpoll_fail_event, function() {
+                fired = true;
+            });
+            var manager = longpoll.getLongPollManager();
+            // Monkeypatch io.
+            manager.io = function(uri, config) {
+                var response = {
+                   responseText: "{non valid json"
+                };
+                config.on.success(2, response);
+            };
+            this.setupLongPoll();
+            Assert.isTrue(fired, "Failure event not fired.");
+        },
+
+        testPollPayLoadOk: function() {
+            // Create a valid message.
+            var custom_event = 'my-event';
+            var custom_payload = {5: 'i'};
+            var custom_response = {
+                'event_key': custom_event,
+                'event_data': custom_payload
+            };
+            var fired = false;
+            Y.on(custom_event, function(data) {
+                fired = true;
+                Assert.areEqual(data, custom_payload);
+            });
+            var manager = longpoll.getLongPollManager();
+            // Monkeypatch io.
+            manager.io = function(uri, config) {
+                var response = {
+                   responseText: Y.JSON.stringify(custom_response)
+                };
+                config.on.success(2, response);
+            };
+            this.setupLongPoll();
+            Assert.isTrue(fired, "Custom event not fired.");
+        }
+
+
+    };
+
+    suite.add(new Y.Test.Case(testLongPoll));
+
+    // Lock, stock, and two smoking barrels.
+    var handle_complete = function(data) {
+        window.status = '::::' + JSON.stringify(data);
+        };
+    Y.Test.Runner.on('complete', handle_complete);
+    Y.Test.Runner.add(suite);
+
+    var console = new Y.Console({newestOnTop: false});
+    console.render('#log');
+
+    Y.on('domready', function() {
+        Y.Test.Runner.run();
+        });
+});

=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt	2011-06-07 20:33:10 +0000
+++ lib/lp/app/templates/base-layout-macros.pt	2011-07-05 17:19:40 +0000
@@ -107,14 +107,17 @@
   <metal:load-lavascript use-macro="context/@@+base-layout-macros/load-javascript" />
 
   <script id="base-layout-load-scripts" type="text/javascript">
-    LPS.use('node', 'event-delegate', 'lp', 'lp.app.links', function(Y) {
+    LPS.use('node', 'event-delegate', 'lp', 'lp.app.links', 'lp.app.longpoll', function(Y) {
         Y.on('load', function(e) {
             sortables_init();
             initInlineHelp();
             Y.lp.activate_collapsibles();
             activateFoldables();
             activateConstrainBugExpiration();
-            Y.lp.app.links.check_valid_lp_links();
+	    Y.lp.app.links.check_valid_lp_links();
+            // Longpolling will only start if
+	    // LP.cache.longpoll is populated.
+            Y.lp.app.longpoll.setupLongPollManager();
         }, window);
 
         // Hook up the function that dismisses the help window if we click


Follow ups