launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06675
[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>