← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-longpoll-js2 into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-longpoll-js2 into lp:maas with lp:~rvb/maas/maas-longpoll-js as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-longpoll-js2/+merge/97263

This is the JavaScript side of longpoll with a few example listeners.  This is mostly a rip-off from Launchpad's longpoll.js module.
-- 
https://code.launchpad.net/~rvb/maas/maas-longpoll-js2/+merge/97263
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-longpoll-js2 into lp:maas.
=== added file 'src/maasserver/static/js/longpoll.js'
--- src/maasserver/static/js/longpoll.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/longpoll.js	2012-03-13 17:52:18 +0000
@@ -0,0 +1,204 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * The Longpoll module provides the functionnality to deal with longpolling
+ * on the JavaScript side.
+ *
+ * The module method setupLongPollManager is the one that should be
+ * called to start the longpolling.
+ *
+ * Then you will probably want to create Javascript handlers for the events
+ * which will be fired:
+ *    - event_key will be fired when the event on the server side is triggered
+ *    (event_key being the name of the event on the server side).
+ *    - see below for other events fired by the longpoll machinery.
+ *
+ * @module longpoll
+ */
+YUI.add('maas.longpoll', function(Y) {
+
+var namespace = Y.namespace('maas.longpoll');
+
+// Event fired when the long polling request starts.
+namespace.longpoll_start_event = 'maas.longpoll.start';
+
+// Event fired each time the long polling request fails (to connect or
+// to parse the returned result).
+namespace.longpoll_fail_event = 'maas.longpoll.failure';
+
+// Event fired when the delay between each failed connection is set to
+// a long delay (after MAX_SHORT_DELAY_FAILED_ATTEMPTS failed attempts).
+namespace.longpoll_longdelay = 'maas.longpoll.longdelay';
+
+// Event fired when the delay between each failed connection is set back
+// to a short delay.
+namespace.longpoll_shortdelay = 'maas.longpoll.shortdelay';
+
+namespace._manager = null;
+
+// After MAX_SHORT_DELAY_FAILED_ATTEMPTS failed connections (real failed
+// connections or connection getting an invalid return) separated
+// by SHORT_DELAY (millisec), wait LONG_DELAY (millisec) between
+// each failed connection.
+namespace.MAX_SHORT_DELAY_FAILED_ATTEMPTS = 5;
+namespace.SHORT_DELAY = 1000;
+namespace.LONG_DELAY = 3*60*1000;
+
+/**
+ *
+ * A Long Poll Manager creates and manages a long polling connexion
+ * to the server to fetch events. This class is not directly used
+ * but managed through 'setupLongPollManager' which creates and
+ * initialises a singleton LongPollManager.
+ *
+ * @class LongPollManager
+ */
+function LongPollManager(config) {
+    LongPollManager.superclass.constructor.apply(this, arguments);
+}
+
+LongPollManager.NAME = "longPollManager";
+
+namespace._repoll = true;
+
+namespace._io = new Y.IO();
+
+Y.extend(LongPollManager, Y.Base, {
+    initializer : function(cfg) {
+        this._started = false;
+        this._failed_attempts = 0;
+        this._sequence = 0;
+    },
+
+    setConnectionInfos : function(key, uri) {
+        this.key = key;
+        this.uri = uri;
+    },
+
+    successPoll : function (id, response) {
+        try {
+            var data = Y.JSON.parse(response.responseText);
+            Y.fire(data.event_key, data);
+            return true;
+        }
+        catch (e) {
+            Y.fire(namespace.longpoll_fail_event, e);
+            return false;
+        }
+    },
+
+    failurePoll : function () {
+        Y.fire(namespace.longpoll_fail_event);
+    },
+
+    /**
+     * Return the delay (milliseconds) to wait before trying to reconnect
+     * again after a failed connection.
+     *
+     * The rationale here is that:
+     * 1. We should not try to reconnect instantaneously after a failed
+     *     connection.
+     * 2. After a certain number of failed connections, we should set the
+     *     delay between two failed connection to a bigger number because
+     *     the server may be having problems.
+     *
+     * @method _pollDelay
+     */
+    _pollDelay : function() {
+        this._failed_attempts = this._failed_attempts + 1;
+        if (this._failed_attempts >=
+                namespace.MAX_SHORT_DELAY_FAILED_ATTEMPTS) {
+            Y.fire(namespace.longpoll_longdelay);
+            Y.log('Switching into long delay mode.');
+            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.
+     */
+    repoll : function(failed) {
+        if (!failed) {
+            if (this._failed_attempts >=
+                    namespace.MAX_SHORT_DELAY_FAILED_ATTEMPTS) {
+                Y.fire(namespace.longpoll_shortdelay);
+            }
+            this._failed_attempts = 0;
+            if (namespace._repoll) {
+                this.poll();
+            }
+        }
+        else {
+            var delay = this._pollDelay();
+            if (namespace._repoll) {
+                Y.later(delay, this, this.poll);
+            }
+        }
+    },
+
+    poll : function() {
+        var that = this;
+        var config = {
+            method: "GET",
+            sync: false,
+            on: {
+                failure: function(id, response) {
+                    if (Y.Lang.isValue(response) &&
+                        Y.Lang.isValue(response.status) &&
+                        (response.status === 408 ||
+                         response.status === 504)) {
+                        // If the error code is:
+                        // - 408 Request timeout
+                        // - 504 Gateway timeout
+                        // Then ignore the error and start
+                        // polling again.
+                        that.repoll(false);
+                    }
+                    else {
+                        that.failurePoll();
+                        that.repoll(true);
+                    }
+                },
+                success: function(id, response) {
+                    var success = that.successPoll(id, response);
+                    that.repoll(!success);
+                }
+            }
+        };
+        this._sequence = this._sequence + 1;
+        var queue_uri = this.uri +
+            "?uuid=" + this.key +
+            "&sequence=" + this._sequence;
+        if (!this._started) {
+            Y.fire(namespace.longpoll_start_event);
+            this._started = true;
+        }
+        namespace._io.send(queue_uri, config);
+    }
+});
+
+namespace.LongPollManager = LongPollManager;
+
+namespace.getLongPollManager = function() {
+    if (!Y.Lang.isValue(namespace._manager)) {
+        namespace._manager = new namespace.LongPollManager();
+    }
+    return namespace._manager;
+};
+
+namespace.setupLongPollManager = function(key, uri) {
+    var longpollmanager = namespace.getLongPollManager();
+    longpollmanager.setConnectionInfos(key, uri);
+    longpollmanager.poll();
+    return longpollmanager;
+};
+
+}, "0.1", {"requires":["base", "event", "json", "io"]});

=== added file 'src/maasserver/static/js/tests/test_longpoll.html'
--- src/maasserver/static/js/tests/test_longpoll.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_longpoll.html	2012-03-13 17:52:18 +0000
@@ -0,0 +1,19 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<html xmlns="http://www.w3.org/1999/xhtml"; xml:lang="en" lang="en">
+  <head>
+    <title>Test maas.longpoll</title>
+
+    <!-- YUI and test setup -->
+    <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
+    <script type="text/javascript" src="../../jslibs/yui/tests/build/yui/yui.js"></script>
+    <script type="text/javascript" src="../testing/testrunner.js"></script>
+    <script type="text/javascript" src="../testing/testing.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>
+  <span id="suite">maas.longpoll.tests</span>
+  </body>
+</html>

=== added file 'src/maasserver/static/js/tests/test_longpoll.js'
--- src/maasserver/static/js/tests/test_longpoll.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_longpoll.js	2012-03-13 17:52:18 +0000
@@ -0,0 +1,253 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ */
+
+YUI({ useBrowserConsole: true }).add('maas.longpoll.tests', function(Y) {
+
+Y.log('loading maas.longpoll.tests');
+var namespace = Y.namespace('maas.longpoll.tests');
+
+var longpoll = Y.maas.longpoll;
+var suite = new Y.Test.Suite("maas.longpoll Tests");
+
+suite.add(new Y.maas.testing.TestCase({
+    name: 'test-longpoll-singleton',
+
+    tearDown: function() {
+        // Cleanup the singleton;
+        longpoll._manager = null;
+    },
+
+    testGetSingletonLongPollManager: function() {
+        Y.Assert.isNull(longpoll._manager);
+        var manager = longpoll.getLongPollManager();
+        Y.Assert.isNotNull(longpoll._manager);
+        var manager2 = longpoll.getLongPollManager();
+        Y.Assert.areSame(manager, manager2);
+    }
+
+}));
+
+suite.add(new Y.maas.testing.TestCase({
+    name: 'test-longpoll',
+
+    setUp: function() {
+        var old_repoll = longpoll._repoll;
+        longpoll._repoll = false;
+        this.addCleanup(function() {longpoll._repoll = old_repoll});
+    },
+
+    tearDown: function() {
+        // Cleanup the singleton.
+        longpoll._manager = null;
+    },
+
+    testInitLongPollManagerQueueName: function() {
+        var manager = longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.areEqual('key', manager.key);
+        Y.Assert.areEqual('/longpoll/', manager.uri);
+        Y.Assert.isFalse(Y.Lang.isValue(manager.nb_calls));
+    },
+
+    testPollStarted: function() {
+        var fired = false;
+        Y.on(longpoll.longpoll_start_event, function() {
+            fired = true;
+        });
+        var manager = longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.isTrue(fired, "Start event not fired.");
+    },
+
+    testPollFailure: function() {
+        var fired = false;
+        Y.on(longpoll.longpoll_fail_event, function() {
+            fired = true;
+        });
+        // Monkeypatch io to simulate failure.
+        var manager = longpoll.getLongPollManager();
+        var mockXhr = new Y.Base();
+        mockXhr.send = function(uri, cfg) {
+            cfg.on.failure();
+        };
+        this.mockIO(mockXhr, longpoll);
+        var manager = longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.isTrue(fired, "Failure event not fired.");
+    },
+
+    testSuccessPollInvalidData: function() {
+        var manager = longpoll.getLongPollManager();
+        var custom_response = "{{";
+        var response = {
+            responseText: custom_response
+        };
+        var res = manager.successPoll("2", response);
+        Y.Assert.isFalse(res);
+    },
+
+    testSuccessPollMalformedData: function() {
+        var manager = longpoll.getLongPollManager();
+        var response = {
+            responseText: '{ "something": "6" }'
+        };
+        var res = manager.successPoll("2", response);
+        Y.Assert.isFalse(res);
+     },
+
+     testSuccessPollWellformedData: function() {
+        var manager = longpoll.getLongPollManager();
+        var response = {
+            responseText: '{ "event_key": "4", "something": "6"}'
+        };
+        var res = manager.successPoll("2", response);
+        Y.Assert.isTrue(res);
+    },
+
+    testPollDelay: function() {
+        // Create event listeners.
+        var longdelay_event_fired = false;
+        Y.on(longpoll.longpoll_longdelay, function(data) {
+            longdelay_event_fired = true;
+        });
+        var shortdelay_event_fired = false;
+        Y.on(longpoll.longpoll_shortdelay, function(data) {
+            shortdelay_event_fired = true;
+        });
+        var manager = longpoll.getLongPollManager();
+        // Monkeypatch io to simulate failure.
+        var mockXhr = new Y.Base();
+        mockXhr.send = function(uri, cfg) {
+            cfg.on.failure();
+        };
+        this.mockIO(mockXhr, longpoll);
+        Y.Assert.areEqual(0, manager._failed_attempts);
+        var manager = longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.areEqual(1, manager._failed_attempts);
+        var i, delay;
+        for (i=0; i<longpoll.MAX_SHORT_DELAY_FAILED_ATTEMPTS-2; i++) {
+            Y.Assert.areEqual(i+1, manager._failed_attempts);
+            delay = manager._pollDelay();
+            Y.Assert.areEqual(delay, longpoll.SHORT_DELAY);
+        }
+        // After MAX_SHORT_DELAY_FAILED_ATTEMPTS failed attempts, the
+        // delay returned by _pollDelay is LONG_DELAY and
+        // longpoll_longdelay is fired.
+        Y.Assert.isFalse(longdelay_event_fired);
+        delay = manager._pollDelay();
+        Y.Assert.isTrue(longdelay_event_fired);
+        Y.Assert.areEqual(delay, longpoll.LONG_DELAY);
+
+        // Monkeypatch io to simulate success.
+        var mockXhr = new Y.Base();
+        mockXhr.send = function(uri, cfg) {
+            var out = {};
+            out.responseText = Y.JSON.stringify({'event_key': 'response'});
+            cfg.on.success(4, out);
+        };
+        this.mockIO(mockXhr, longpoll);
+        // After a success, longpoll.longpoll_shortdelay is fired.
+        Y.Assert.isFalse(shortdelay_event_fired);
+        delay = manager.poll();
+        Y.Assert.isTrue(shortdelay_event_fired);
+    },
+
+    testPollUriSequence: function() {
+        // Each new polling increases 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();
+        var mockXhr = new Y.Base();
+        mockXhr.send = function(uri, cfg) {
+            Y.Assert.areEqual(
+                '/longpoll/?uuid=key&sequence=' + (count+1),
+                uri);
+            count = count + 1;
+            var response = {
+               responseText: '{"i":2}'
+            };
+            cfg.on.success(2, response);
+        };
+        this.mockIO(mockXhr, longpoll);
+        longpoll.setupLongPollManager('key', '/longpoll/');
+        var request;
+        for (request=1; request<10; request++) {
+            Y.Assert.isTrue(count === request, "Uri not requested.");
+            manager.poll();
+        }
+    },
+
+    _testDoesNotFail: function(error_code) {
+        // Assert that, when the longpoll request receives an error
+        // with code error_code, it is not treated as a failed
+        // connection attempt.
+        var manager = longpoll.getLongPollManager();
+        // Monkeypatch io to simulate a request timeout.
+        var mockXhr = new Y.Base();
+        mockXhr.send = function(uri, cfg) {
+            response = {status: error_code};
+            cfg.on.failure(4, response);
+        };
+        this.mockIO(mockXhr, longpoll);
+
+        Y.Assert.areEqual(0, manager._failed_attempts);
+        longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.areEqual(0, manager._failed_attempts);
+    },
+
+    test408RequestTimeoutHandling: function() {
+        this._testDoesNotFail(408);
+    },
+
+    test504GatewayTimeoutHandling: function() {
+        this._testDoesNotFail(504);
+    },
+
+    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();
+        var response = "{non valid json";
+        this.mockSuccess(response, longpoll);
+        longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.isTrue(fired, "Failure event not fired.");
+    },
+
+    testPollPayLoadOk: function() {
+        // Create a valid message.
+        var custom_response = {
+            'event_key': 'my-event',
+            'something': {something_else: 1234}
+        };
+        var fired = false;
+        Y.on(custom_response.event_key, function(data) {
+            fired = true;
+            Y.Assert.areEqual(data, custom_response);
+        });
+        var manager = longpoll.getLongPollManager();
+        // Monkeypatch io.
+        var mockXhr = new Y.Base();
+        mockXhr.send = function(uri, cfg) {
+            var out = {};
+            out.responseText = Y.JSON.stringify(custom_response);
+            cfg.on.success(4, out);
+        };
+        this.mockIO(mockXhr, longpoll);
+        longpoll.setupLongPollManager('key', '/longpoll/');
+        Y.Assert.isTrue(fired, "Custom event not fired.");
+    }
+
+}));
+
+
+namespace.suite = suite;
+
+}, '0.1', {'requires': [
+    'node-event-simulate', 'test', 'maas.testing', 'maas.longpoll']}
+);

=== modified file 'src/maasserver/templates/maasserver/index.html'
--- src/maasserver/templates/maasserver/index.html	2012-03-06 12:31:34 +0000
+++ src/maasserver/templates/maasserver/index.html	2012-03-13 17:52:18 +0000
@@ -12,6 +12,7 @@
   <!--
   YUI().use(
     'maas.node_add', 'maas.node','maas.node_views', 'maas.utils',
+    'maas.longpoll',
     function (Y) {
     Y.on('load', function() {
       // Create Dashboard view.
@@ -35,6 +36,26 @@
 	  {srcNode: '.page-title-form'});
       title_widget.render();
 
+      // Sample event listeners.
+      Y.on("Node.updated", function() {
+		  Y.log("node updated");
+      });
+
+      Y.on("Node.created", function() {
+		  Y.log("node created");
+      });
+
+      Y.on("Node.deleted", function() {
+		  Y.log("node deleted");
+      });
+
+      // Start longpoll.
+      {% if longpoll_queue and LONGPOLL_URL %}
+        Y.later(0, Y.maas.longpoll, function() {
+          var longpollmanager = Y.maas.longpoll.setupLongPollManager(
+            '{{ longpoll_queue }}', '/{{ LONGPOLL_URL }}');
+        });
+      {% endif %}
     });
   });
   // -->

=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
--- src/maasserver/templates/maasserver/js-conf.html	2012-03-13 17:52:18 +0000
+++ src/maasserver/templates/maasserver/js-conf.html	2012-03-13 17:52:18 +0000
@@ -28,4 +28,5 @@
 <script type="text/javascript" src="{{ STATIC_URL }}js/prefs.js"></script>
 <script type="text/javascript" src="{{ STATIC_URL }}js/utils.js"></script>
 <script type="text/javascript" src="{{ STATIC_URL }}js/node_views.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}js/longpoll.js"></script>