Merge ~mpontillo/maas:enable-ping--js--bug-1802325 into maas:master
- Git
- lp:~mpontillo/maas
- enable-ping--js--bug-1802325
- Merge into master
Proposed by
Mike Pontillo
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | 1d2b6342e292e7d619704807dcb8e8e85e87e4df |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~mpontillo/maas:enable-ping--js--bug-1802325 |
Merge into: | maas:master |
Prerequisite: | ~mpontillo/maas:add-websocket-ping-ability--bug-1802325 |
Diff against target: |
978 lines (+394/-174) 11 files modified
.gitattributes (+5/-0) src/maasserver/static/js/angular/directives/tests/test_boot_images.js (+5/-0) src/maasserver/static/js/angular/directives/tests/test_ipranges.js (+4/-0) src/maasserver/static/js/angular/directives/tests/test_notifications.js (+5/-1) src/maasserver/static/js/angular/directives/tests/test_pod_parameters.js (+4/-0) src/maasserver/static/js/angular/directives/tests/test_ssh_keys.js (+4/-0) src/maasserver/static/js/angular/factories/region.js (+232/-123) src/maasserver/static/js/angular/factories/tests/test_region.js (+101/-41) src/maasserver/static/js/angular/services/log.js (+1/-1) src/maasserver/static/js/angular/testing/websocket.js (+32/-8) webpack.config.js (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+358889@code.launchpad.net |
Commit message
Add connectivity check and retry behavior to web UI.
* Periodically send WebSocket requests to ensure connectivity.
* Attempt to retry connection if requests are not answered in a timely manner.
* Fix tests to ensure real WebSocket connections are not attempted.
* Change .gitattributes to ensure built JavaScript and bundles and CSS are not included in text-based diff output.
* Enable cache to make webpack builds faster.
Partial fix for bug #1802325.
Description of the change
To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Like the ES6 class, so much nicer than that old JS crap.
review:
Approve
- e9712be... by Mike Pontillo
-
Remove excessive debug logging.
- 1d2b634... by Mike Pontillo
-
Clear out event handlers so we don't get events from stale websockets.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/.gitattributes b/.gitattributes |
2 | index 5c54628..7d16138 100644 |
3 | --- a/.gitattributes |
4 | +++ b/.gitattributes |
5 | @@ -7,3 +7,8 @@ |
6 | /services export-ignore |
7 | .gitignore export-ignore |
8 | .gitattributes export-ignore |
9 | + |
10 | +# Don't include generated JavaScript or CSS in diff output. |
11 | +# (They will still show up as binary files that differ.) |
12 | +/src/maasserver/static/js/bundle/** -diff |
13 | +/src/maasserver/static/css/build.css -diff |
14 | diff --git a/src/maasserver/static/js/angular/directives/tests/test_boot_images.js b/src/maasserver/static/js/angular/directives/tests/test_boot_images.js |
15 | index c569220..91471c8 100644 |
16 | --- a/src/maasserver/static/js/angular/directives/tests/test_boot_images.js |
17 | +++ b/src/maasserver/static/js/angular/directives/tests/test_boot_images.js |
18 | @@ -24,6 +24,11 @@ describe("maasBootImages", function() { |
19 | BootResourcesManager = $injector.get('BootResourcesManager'); |
20 | UsersManager = $injector.get('UsersManager'); |
21 | ManagerHelperService = $injector.get('ManagerHelperService'); |
22 | + // Mock buildSocket so an actual connection is not made. |
23 | + let RegionConnection = $injector.get("RegionConnection"); |
24 | + let webSocket = new MockWebSocket(); |
25 | + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); |
26 | + |
27 | })); |
28 | |
29 | // Create a new scope before each test. |
30 | diff --git a/src/maasserver/static/js/angular/directives/tests/test_ipranges.js b/src/maasserver/static/js/angular/directives/tests/test_ipranges.js |
31 | index db8f812..327f3f8 100644 |
32 | --- a/src/maasserver/static/js/angular/directives/tests/test_ipranges.js |
33 | +++ b/src/maasserver/static/js/angular/directives/tests/test_ipranges.js |
34 | @@ -24,6 +24,10 @@ describe("maasIPRanges", function() { |
35 | IPRangesManager = $injector.get('IPRangesManager'); |
36 | UsersManager = $injector.get('UsersManager'); |
37 | ManagerHelperService = $injector.get('ManagerHelperService'); |
38 | + // Mock buildSocket so an actual connection is not made. |
39 | + let RegionConnection = $injector.get("RegionConnection"); |
40 | + let webSocket = new MockWebSocket(); |
41 | + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); |
42 | })); |
43 | |
44 | // Create a new scope before each test. |
45 | diff --git a/src/maasserver/static/js/angular/directives/tests/test_notifications.js b/src/maasserver/static/js/angular/directives/tests/test_notifications.js |
46 | index 22c7532..e7704b8 100644 |
47 | --- a/src/maasserver/static/js/angular/directives/tests/test_notifications.js |
48 | +++ b/src/maasserver/static/js/angular/directives/tests/test_notifications.js |
49 | @@ -62,9 +62,13 @@ describe("maasNotifications", function() { |
50 | var theNotificationsManager; |
51 | var $scope; |
52 | |
53 | - beforeEach(inject(function($rootScope, NotificationsManager) { |
54 | + beforeEach(inject(function($rootScope, NotificationsManager, $injector) { |
55 | theNotificationsManager = NotificationsManager; |
56 | $scope = $rootScope.$new(); |
57 | + // Mock buildSocket so an actual connection is not made. |
58 | + let RegionConnection = $injector.get("RegionConnection"); |
59 | + let webSocket = new MockWebSocket(); |
60 | + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); |
61 | })); |
62 | |
63 | describe("maas-notifications", function() { |
64 | diff --git a/src/maasserver/static/js/angular/directives/tests/test_pod_parameters.js b/src/maasserver/static/js/angular/directives/tests/test_pod_parameters.js |
65 | index 618bb5c..e1f1774 100644 |
66 | --- a/src/maasserver/static/js/angular/directives/tests/test_pod_parameters.js |
67 | +++ b/src/maasserver/static/js/angular/directives/tests/test_pod_parameters.js |
68 | @@ -15,6 +15,10 @@ describe("maasPodParameters", function() { |
69 | PodsManager = $injector.get("PodsManager"); |
70 | GeneralManager = $injector.get("GeneralManager"); |
71 | ManagerHelperService = $injector.get("ManagerHelperService"); |
72 | + // Mock buildSocket so an actual connection is not made. |
73 | + let RegionConnection = $injector.get("RegionConnection"); |
74 | + let webSocket = new MockWebSocket(); |
75 | + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); |
76 | })); |
77 | |
78 | // Create a new scope before each test. |
79 | diff --git a/src/maasserver/static/js/angular/directives/tests/test_ssh_keys.js b/src/maasserver/static/js/angular/directives/tests/test_ssh_keys.js |
80 | index 8783f61..d886f5d 100644 |
81 | --- a/src/maasserver/static/js/angular/directives/tests/test_ssh_keys.js |
82 | +++ b/src/maasserver/static/js/angular/directives/tests/test_ssh_keys.js |
83 | @@ -25,6 +25,10 @@ describe("maasSshKeys", function() { |
84 | SSHKeysManager = $injector.get('SSHKeysManager'); |
85 | JSONService = $injector.get('JSONService'); |
86 | ManagerHelperService = $injector.get('ManagerHelperService'); |
87 | + // Mock buildSocket so an actual connection is not made. |
88 | + let RegionConnection = $injector.get("RegionConnection"); |
89 | + let webSocket = new MockWebSocket(); |
90 | + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); |
91 | })); |
92 | |
93 | // Create a new scope before each test. |
94 | diff --git a/src/maasserver/static/js/angular/factories/region.js b/src/maasserver/static/js/angular/factories/region.js |
95 | index 9e1e8a9..eaa78ca 100644 |
96 | --- a/src/maasserver/static/js/angular/factories/region.js |
97 | +++ b/src/maasserver/static/js/angular/factories/region.js |
98 | @@ -1,4 +1,4 @@ |
99 | -/* Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
100 | +/* Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
101 | * GNU Affero General Public License version 3 (see the file LICENSE). |
102 | * |
103 | * MAAS Region Connection |
104 | @@ -7,34 +7,58 @@ |
105 | * service. |
106 | */ |
107 | |
108 | +// Message types |
109 | +const MSG_TYPE = { |
110 | + REQUEST: 0, |
111 | + RESPONSE: 1, |
112 | + NOTIFY: 2, |
113 | + PING: 3, |
114 | + PING_REPLY: 4 |
115 | +}; |
116 | + |
117 | +// Response types |
118 | +const RESPONSE_TYPE = { |
119 | + SUCCESS: 0, |
120 | + ERROR: 1 |
121 | +}; |
122 | + |
123 | +const REGION_STATE = { |
124 | + DOWN: 0, |
125 | + UP: 1, |
126 | + RETRY: 2 |
127 | +}; |
128 | + |
129 | angular.module('MAAS').factory( |
130 | - 'RegionConnection', |
131 | - ['$q', '$rootScope', '$timeout', '$window', '$cookies', function( |
132 | - $q, $rootScope, $timeout, $window, $cookies) { |
133 | - |
134 | - // Message types |
135 | - var MSG_TYPE = { |
136 | - REQUEST: 0, |
137 | - RESPONSE: 1, |
138 | - NOTIFY: 2 |
139 | - }; |
140 | - |
141 | - // Response types |
142 | - var RESPONSE_TYPE = { |
143 | - SUCCESS: 0, |
144 | - ERROR: 1 |
145 | - }; |
146 | - |
147 | - // Constructor |
148 | - function RegionConnection() { |
149 | + 'RegionConnection', [ |
150 | + '$q', '$rootScope', '$timeout', '$window', '$cookies', 'LogService', |
151 | + function() { return new RegionConnection(...arguments); } |
152 | + ] |
153 | +); |
154 | + |
155 | + class RegionConnection { |
156 | + |
157 | + constructor($q, $rootScope, $timeout, $window, $cookies, LogService) { |
158 | + this.$q = $q; |
159 | + this.$rootScope = $rootScope; |
160 | + this.$timeout = $timeout; |
161 | + this.$window = $window; |
162 | + this.$cookies = $cookies; |
163 | + this.log = LogService; |
164 | + |
165 | + // Expose this constant so the tests can access it. |
166 | + this.STATE = REGION_STATE; |
167 | + |
168 | this.callbacks = {}; |
169 | this.requests = {}; |
170 | + this.pingsInFlight = new Set(); |
171 | this.requestId = 0; |
172 | this.url = null; |
173 | this.websocket = null; |
174 | - this.connected = false; |
175 | - this.autoReconnect = true; |
176 | - this.retryTimeout = 5000; |
177 | + this.state = REGION_STATE.DOWN; |
178 | + this.ensureConnectionPromise = null; |
179 | + this.connectionCheckInterval = 5000; |
180 | + this.maxMissedPings = 5; |
181 | + this.maxPatience = 5; |
182 | this.error = null; |
183 | |
184 | // Defer used for defaultConnect. If defaultConnect is called |
185 | @@ -58,13 +82,13 @@ angular.module('MAAS').factory( |
186 | } |
187 | |
188 | // Return a new request id. |
189 | - RegionConnection.prototype.newRequestId = function() { |
190 | + newRequestId() { |
191 | this.requestId += 1; |
192 | return this.requestId; |
193 | - }; |
194 | + } |
195 | |
196 | // Register event handler. |
197 | - RegionConnection.prototype.registerHandler = function (name, func) { |
198 | + registerHandler(name, func) { |
199 | if(!angular.isDefined(this.handlers[name])) { |
200 | throw new Error("Invalid handler: " + name); |
201 | } |
202 | @@ -72,21 +96,21 @@ angular.module('MAAS').factory( |
203 | throw new Error("Requires a function to register a handler."); |
204 | } |
205 | this.handlers[name].push(func); |
206 | - }; |
207 | + } |
208 | |
209 | // Unregister event handler. |
210 | - RegionConnection.prototype.unregisterHandler = function (name, func) { |
211 | + unregisterHandler(name, func) { |
212 | if(!angular.isDefined(this.handlers[name])) { |
213 | throw new Error("Invalid handler: " + name); |
214 | } |
215 | - var idx = this.handlers[name].indexOf(func); |
216 | + let idx = this.handlers[name].indexOf(func); |
217 | if(idx >= 0) { |
218 | this.handlers[name].splice(idx, 1); |
219 | } |
220 | - }; |
221 | + } |
222 | |
223 | // Register notification handler. |
224 | - RegionConnection.prototype.registerNotifier = function(name, func) { |
225 | + registerNotifier(name, func) { |
226 | if(!angular.isFunction(func)) { |
227 | throw new Error("Requires a function to register a notifier."); |
228 | } |
229 | @@ -94,96 +118,173 @@ angular.module('MAAS').factory( |
230 | this.notifiers[name] = []; |
231 | } |
232 | this.notifiers[name].push(func); |
233 | - }; |
234 | + } |
235 | |
236 | // Unregister notification handler. |
237 | - RegionConnection.prototype.unregisterNotifier = function(name, func) { |
238 | + unregisterNotifier(name, func) { |
239 | if(angular.isUndefined(this.notifiers[name])) { |
240 | return; |
241 | } |
242 | - var idx = this.notifiers[name].indexOf(func); |
243 | + let idx = this.notifiers[name].indexOf(func); |
244 | if(idx >= 0) { |
245 | this.notifiers[name].splice(idx, 1); |
246 | } |
247 | - }; |
248 | + } |
249 | |
250 | // Return True if currently connected to region. |
251 | - RegionConnection.prototype.isConnected = function() { |
252 | - return this.connected; |
253 | - }; |
254 | + isConnected() { |
255 | + return this.state === REGION_STATE.UP; |
256 | + } |
257 | |
258 | // Builds the websocket connection. |
259 | - RegionConnection.prototype.buildSocket = function(url) { |
260 | + buildSocket(url) { |
261 | return new WebSocket(url); |
262 | - }; |
263 | + } |
264 | + |
265 | + send(request) { |
266 | + if(this.state !== REGION_STATE.UP) { |
267 | + this.log.warn( |
268 | + "Sent request while region connection not available:", |
269 | + request); |
270 | + } |
271 | + // XXX mpontillo 2018-11-19: really we should handle errors here |
272 | + // in a more robust way, but that's a bigger change. |
273 | + this.websocket.send(angular.toJson(request)); |
274 | + } |
275 | + |
276 | + ping() { |
277 | + let request_id = this.newRequestId(); |
278 | + let request = { |
279 | + type: MSG_TYPE.PING, |
280 | + request_id: request_id, |
281 | + }; |
282 | + this.pingsInFlight.add(request_id); |
283 | + this.send(request); |
284 | + } |
285 | + |
286 | + scheduleEnsureConnection() { |
287 | + if(this.ensureConnectionPromise) { |
288 | + this.$timeout.cancel(this.ensureConnectionPromise); |
289 | + this.ensureConnectionPromise = null; |
290 | + } |
291 | + this.ensureConnectionPromise = this.$timeout( |
292 | + this.ensureConnection.bind(this), |
293 | + this.connectionCheckInterval); |
294 | + } |
295 | + |
296 | + ensureConnection() { |
297 | + this.scheduleEnsureConnection(); |
298 | + if(this.state === REGION_STATE.UP) { |
299 | + let missedPings = this.pingsInFlight.size; |
300 | + let outstandingRequests = Object.keys(this.requests).length; |
301 | + let impatienceFactor = missedPings + outstandingRequests; |
302 | + if(missedPings > 0) { |
303 | + this.log.debug( |
304 | + "Still waiting for ping replies: ", |
305 | + this.pingsInFlight); |
306 | + } |
307 | + if(missedPings >= this.maxMissedPings) { |
308 | + // Assume the connection has timed out. |
309 | + this.log.warn("Closed connection: ping timeout."); |
310 | + this.retry(); |
311 | + } else if(missedPings > 0 && |
312 | + impatienceFactor >= this.maxPatience) { |
313 | + this.log.warn( |
314 | + "Closed connection: impatience factor exceeded."); |
315 | + this.retry(); |
316 | + } |
317 | + else { |
318 | + this.ping(); |
319 | + } |
320 | + } else if(this.state === REGION_STATE.RETRY) { |
321 | + // We know we're already in RETRY state, but call retry() |
322 | + // again to ensure we don't continue to create additional |
323 | + // WebSocket objects (without closing them). |
324 | + this.retry(); |
325 | + this.log.debug("Attempting to reconnect..."); |
326 | + this.connect(); |
327 | + } |
328 | + } |
329 | |
330 | // Opens the websocket connection. |
331 | - RegionConnection.prototype.connect = function() { |
332 | + connect() { |
333 | this.url = this._buildUrl(); |
334 | - this.autoReconnect = true; |
335 | this.websocket = this.buildSocket(this.url); |
336 | |
337 | - var self = this; |
338 | - this.websocket.onopen = function(evt) { |
339 | - self.connected = true; |
340 | - angular.forEach(self.handlers.open, function(func) { |
341 | + this.websocket.onopen = (evt) => { |
342 | + this.state = REGION_STATE.UP; |
343 | + this.pingsInFlight.clear(); |
344 | + this.scheduleEnsureConnection(); |
345 | + angular.forEach(this.handlers.open, (func) => { |
346 | func(evt); |
347 | }); |
348 | + this.log.debug("WebSocket connection established."); |
349 | }; |
350 | - this.websocket.onerror = function(evt) { |
351 | - angular.forEach(self.handlers.error, function(func) { |
352 | + this.websocket.onerror = (evt) => { |
353 | + this.log.error("WebSocket error: ", evt); |
354 | + angular.forEach(this.handlers.error, (func) => { |
355 | func(evt); |
356 | }); |
357 | }; |
358 | - this.websocket.onclose = function(evt) { |
359 | - self.connected = false; |
360 | - self.error = "Unable to connect to: " + self.url.split("?")[0]; |
361 | - angular.forEach(self.handlers.close, function(func) { |
362 | + this.websocket.onclose = (evt) => { |
363 | + let url = this.url.split("?")[0]; |
364 | + this.log.warn("WebSocket connection closed: " + url); |
365 | + angular.forEach(this.handlers.close, (func) => { |
366 | func(evt); |
367 | }); |
368 | - if(self.autoReconnect) { |
369 | - $timeout(function() { |
370 | - self.connect(); |
371 | - }, self.retryTimeout); |
372 | - } |
373 | + this.websocket = null; |
374 | + this.retry(); |
375 | }; |
376 | - this.websocket.onmessage = function(evt) { |
377 | - self.onMessage(angular.fromJson(evt.data)); |
378 | + this.websocket.onmessage = (evt) => { |
379 | + this.onMessage(angular.fromJson(evt.data)); |
380 | }; |
381 | - }; |
382 | + } |
383 | |
384 | - // Closes the websocket connection. |
385 | - RegionConnection.prototype.close = function() { |
386 | - this.autoReconnect = false; |
387 | - this.websocket.close(); |
388 | - this.websocket = null; |
389 | - }; |
390 | + // Closes the websocket connection and begin to retry the connection. |
391 | + retry() { |
392 | + // Ensure the socket is closed, if applicable. If the WebSocket |
393 | + // is already null (such as what might happen if this method is |
394 | + // called from the onclose() hanlder), no need to close it again. |
395 | + if(this.websocket) { |
396 | + // Clear out event handlers. By now we already know the |
397 | + // websocket is closed; we don't want stray events from the |
398 | + // now-dead websocket to affect the existing connection. |
399 | + this.websocket.onopen = null; |
400 | + this.websocket.onerror = null; |
401 | + this.websocket.onclose = null; |
402 | + this.websocket.close(); |
403 | + this.websocket = null; |
404 | + } |
405 | + // Set the state to RETRY so that ensureConnection will attempt |
406 | + // to reconnect the next time it runs. |
407 | + this.state = REGION_STATE.RETRY; |
408 | + } |
409 | |
410 | // Return the protocol used for the websocket connection. |
411 | - RegionConnection.prototype._getProtocol = function() { |
412 | - return $window.location.protocol; |
413 | - }; |
414 | + _getProtocol() { |
415 | + return this.$window.location.protocol; |
416 | + } |
417 | |
418 | // Return connection url to websocket from current location and |
419 | // html options. |
420 | - RegionConnection.prototype._buildUrl = function() { |
421 | - var host = $window.location.hostname; |
422 | - var port = $window.location.port; |
423 | - var path = $window.location.pathname; |
424 | - var proto = 'ws'; |
425 | + _buildUrl() { |
426 | + let host = this.$window.location.hostname; |
427 | + let port = this.$window.location.port; |
428 | + let path = this.$window.location.pathname; |
429 | + let proto = 'ws'; |
430 | if (this._getProtocol() === 'https:') { |
431 | proto = 'wss'; |
432 | } |
433 | |
434 | // Path and port can be overridden by href and data-websocket-port |
435 | // in the base element respectively. |
436 | - var base = angular.element("base"); |
437 | + let base = angular.element("base"); |
438 | if(angular.isDefined(base)) { |
439 | - var newPath = base.attr("href"); |
440 | + let newPath = base.attr("href"); |
441 | if(angular.isDefined(newPath)) { |
442 | path = newPath; |
443 | } |
444 | - var newPort = base.data("websocket-port"); |
445 | + let newPort = base.data("websocket-port"); |
446 | if(angular.isDefined(newPort)) { |
447 | port = newPort; |
448 | } |
449 | @@ -195,122 +296,130 @@ angular.module('MAAS').factory( |
450 | } |
451 | |
452 | // Build the URL. Include the :port only if it has a value. |
453 | - url = proto + "://" + host; |
454 | - if(angular.isString(port) && port.length > 0){ |
455 | + let url = proto + "://" + host; |
456 | + if(angular.isString(port) && port.length > 0) { |
457 | url += ":" + port; |
458 | } |
459 | url += path + "ws"; |
460 | |
461 | // Include the csrftoken in the URL if it's defined. |
462 | - var csrftoken; |
463 | - if(angular.isFunction($cookies.get)) { |
464 | - csrftoken = $cookies.get('csrftoken'); |
465 | + let csrftoken; |
466 | + if(angular.isFunction(this.$cookies.get)) { |
467 | + csrftoken = this.$cookies.get('csrftoken'); |
468 | } else { |
469 | - csrftoken = $cookies.csrftoken; |
470 | + csrftoken = this.$cookies.csrftoken; |
471 | } |
472 | if(angular.isDefined(csrftoken)) { |
473 | url += '?csrftoken=' + encodeURIComponent(csrftoken); |
474 | } |
475 | |
476 | return url; |
477 | - }; |
478 | + } |
479 | |
480 | // Opens the default websocket connection. |
481 | - RegionConnection.prototype.defaultConnect = function() { |
482 | + defaultConnect() { |
483 | // Already been called but the connection has not been completed. |
484 | if(angular.isObject(this.defaultConnectDefer)) { |
485 | return this.defaultConnectDefer.promise; |
486 | } |
487 | |
488 | // Already connected. |
489 | - var defer; |
490 | + let defer; |
491 | if(this.isConnected()) { |
492 | // Create a new defer as the defaultConnectDefer would |
493 | // have already been resolved. |
494 | - defer = $q.defer(); |
495 | + defer = this.$q.defer(); |
496 | |
497 | // Cannot resolve the defer inline as it hasn't been given |
498 | // back to the caller. It will be called in the next loop. |
499 | - $timeout(defer.resolve); |
500 | + this.$timeout(defer.resolve); |
501 | return defer.promise; |
502 | } |
503 | |
504 | // Start the connection. |
505 | - var self = this, opened, errored; |
506 | - defer = this.defaultConnectDefer = $q.defer(); |
507 | - opened = function(evt) { |
508 | + defer = this.defaultConnectDefer = this.$q.defer(); |
509 | + let opened = (evt) => { |
510 | this.defaultConnectDefer = null; |
511 | - self.unregisterHandler("open", opened); |
512 | - self.unregisterHandler("error", errored); |
513 | - $rootScope.$apply(defer.resolve(evt)); |
514 | + this.unregisterHandler("open", opened); |
515 | + this.unregisterHandler("error", errored); |
516 | + this.$rootScope.$apply(defer.resolve(evt)); |
517 | }; |
518 | - errored = function(evt) { |
519 | + let errored = (evt) => { |
520 | this.defaultConnectDefer = null; |
521 | - self.unregisterHandler("open", opened); |
522 | - self.unregisterHandler("error", errored); |
523 | - $rootScope.$apply(defer.reject(evt)); |
524 | + this.unregisterHandler("open", opened); |
525 | + this.unregisterHandler("error", errored); |
526 | + this.$rootScope.$apply(defer.reject(evt)); |
527 | }; |
528 | this.registerHandler("open", opened); |
529 | this.registerHandler("error", errored); |
530 | this.connect(); |
531 | return defer.promise; |
532 | - }; |
533 | + } |
534 | |
535 | // Called when a message is received. |
536 | - RegionConnection.prototype.onMessage = function(msg) { |
537 | - // Response |
538 | + onMessage(msg) { |
539 | + // Synchronous response |
540 | if(msg.type === MSG_TYPE.RESPONSE) { |
541 | this.onResponse(msg); |
542 | - // Notify |
543 | + // Asynchronous notification |
544 | } else if(msg.type === MSG_TYPE.NOTIFY) { |
545 | this.onNotify(msg); |
546 | + // Reply to connectivity check |
547 | + } else if(msg.type === MSG_TYPE.PING_REPLY) { |
548 | + this.onPingReply(msg); |
549 | } |
550 | - }; |
551 | + } |
552 | |
553 | // Called when a response message is recieved. |
554 | - RegionConnection.prototype.onResponse = function(msg) { |
555 | + onResponse(msg) { |
556 | // Grab the registered defer from the callbacks list. |
557 | - var defer = this.callbacks[msg.request_id]; |
558 | - var remembered_request = this.requests[msg.request_id]; |
559 | + let defer = this.callbacks[msg.request_id]; |
560 | + let remembered_request = this.requests[msg.request_id]; |
561 | if(angular.isDefined(defer)) { |
562 | if(msg.rtype === RESPONSE_TYPE.SUCCESS) { |
563 | // Resolve the defer inside of the digest cycle, so any |
564 | // update to an object or collection will trigger a |
565 | // watcher. |
566 | - $rootScope.$apply(defer.resolve(msg.result)); |
567 | + this.$rootScope.$apply(defer.resolve(msg.result)); |
568 | } else if(msg.rtype === RESPONSE_TYPE.ERROR) { |
569 | // Reject the defer since an error occurred. |
570 | if(angular.isObject(remembered_request)) { |
571 | - $rootScope.$apply(defer.reject({ |
572 | + this.$rootScope.$apply(defer.reject({ |
573 | "error": msg.error, |
574 | "request": remembered_request |
575 | })); |
576 | } else { |
577 | - $rootScope.$apply(defer.reject(msg.error)); |
578 | + this.$rootScope.$apply(defer.reject(msg.error)); |
579 | } |
580 | } |
581 | // Remove the defer from the callback list. |
582 | delete this.callbacks[msg.request_id]; |
583 | delete this.requests[msg.request_id]; |
584 | } |
585 | - }; |
586 | + } |
587 | |
588 | // Called when a notify response is recieved. |
589 | - RegionConnection.prototype.onNotify = function(msg) { |
590 | - var handlers = this.notifiers[msg.name]; |
591 | + onNotify(msg) { |
592 | + let handlers = this.notifiers[msg.name]; |
593 | if(angular.isArray(handlers)) { |
594 | angular.forEach(handlers, function(handler) { |
595 | handler(msg.action, msg.data); |
596 | }); |
597 | } |
598 | - }; |
599 | + } |
600 | + |
601 | + onPingReply(msg) { |
602 | + // Note: The msg.result at this point contains the last sequence |
603 | + // number received, but it isn't really relevant for us. It could |
604 | + // be helpful for debug logging, if necessary. |
605 | + this.pingsInFlight.delete(msg.request_id); |
606 | + } |
607 | |
608 | // Call method on the region. |
609 | - RegionConnection.prototype.callMethod = function( |
610 | - method, params, remember) { |
611 | - var defer = $q.defer(); |
612 | - var request_id = this.newRequestId(); |
613 | - var request = { |
614 | + callMethod(method, params, remember) { |
615 | + let defer = this.$q.defer(); |
616 | + let request_id = this.newRequestId(); |
617 | + let request = { |
618 | type: MSG_TYPE.REQUEST, |
619 | request_id: request_id, |
620 | method: method, |
621 | @@ -322,9 +431,9 @@ angular.module('MAAS').factory( |
622 | if (remember) { |
623 | this.requests[request_id] = request; |
624 | } |
625 | - this.websocket.send(angular.toJson(request)); |
626 | + this.send(request); |
627 | + // Uncomment this to log every WebSocket method call. |
628 | + // this.log.debug("callMethod(): ", request, remember); |
629 | return defer.promise; |
630 | - }; |
631 | - |
632 | - return new RegionConnection(); |
633 | - }]); |
634 | + } |
635 | + } |
636 | diff --git a/src/maasserver/static/js/angular/factories/tests/test_region.js b/src/maasserver/static/js/angular/factories/tests/test_region.js |
637 | index c5e6d8c..57db53a 100644 |
638 | --- a/src/maasserver/static/js/angular/factories/tests/test_region.js |
639 | +++ b/src/maasserver/static/js/angular/factories/tests/test_region.js |
640 | @@ -20,7 +20,7 @@ describe("RegionConnection", function() { |
641 | })); |
642 | |
643 | // Load the RegionConnection factory. |
644 | - var RegionConnection, webSocket; |
645 | + let RegionConnection, webSocket; |
646 | beforeEach(inject(function($injector) { |
647 | RegionConnection = $injector.get("RegionConnection"); |
648 | |
649 | @@ -186,17 +186,93 @@ describe("RegionConnection", function() { |
650 | |
651 | describe("isConnected", function() { |
652 | |
653 | - it("returns true", function() { |
654 | - RegionConnection.connected = true; |
655 | + it("returns true in STATE.UP", function() { |
656 | + RegionConnection.state = RegionConnection.STATE.UP; |
657 | expect(RegionConnection.isConnected()).toBe(true); |
658 | }); |
659 | |
660 | - it("returns false", function() { |
661 | - RegionConnection.connected = false; |
662 | + it("returns false in STATE.DOWN", function() { |
663 | + RegionConnection.state = RegionConnection.STATE.DOWN; |
664 | + expect(RegionConnection.isConnected()).toBe(false); |
665 | + }); |
666 | + |
667 | + it("returns false in STATE.RETRY", function() { |
668 | + RegionConnection.state = RegionConnection.STATE.RETRY; |
669 | expect(RegionConnection.isConnected()).toBe(false); |
670 | }); |
671 | }); |
672 | |
673 | + describe("scheduleEnsureConnection", function() { |
674 | + |
675 | + it("schedules ensureConnection to run", function() { |
676 | + spyOn(RegionConnection, "ensureConnection"); |
677 | + RegionConnection.scheduleEnsureConnection(); |
678 | + $timeout.flush(); |
679 | + expect(RegionConnection.ensureConnection).toHaveBeenCalled(); |
680 | + }); |
681 | + |
682 | + it("does not schedule itself multiple times", function() { |
683 | + spyOn(RegionConnection, "ensureConnection"); |
684 | + RegionConnection.scheduleEnsureConnection(); |
685 | + RegionConnection.scheduleEnsureConnection(); |
686 | + RegionConnection.scheduleEnsureConnection(); |
687 | + RegionConnection.scheduleEnsureConnection(); |
688 | + RegionConnection.scheduleEnsureConnection(); |
689 | + $timeout.flush(); |
690 | + expect(RegionConnection.ensureConnection).toHaveBeenCalledTimes(1); |
691 | + }); |
692 | + |
693 | + }); |
694 | + |
695 | + describe("ensureConnection", function() { |
696 | + |
697 | + beforeEach(function() { |
698 | + spyOn(RegionConnection, "scheduleEnsureConnection"); |
699 | + spyOn(RegionConnection, "retry"); |
700 | + spyOn(RegionConnection, "connect"); |
701 | + spyOn(RegionConnection, "ping"); |
702 | + }); |
703 | + |
704 | + it("schedules itself to run again later", function() { |
705 | + RegionConnection.ensureConnection(); |
706 | + expect( |
707 | + RegionConnection.scheduleEnsureConnection).toHaveBeenCalled(); |
708 | + }); |
709 | + |
710 | + it("calls retry() if in UP state with missed pings", function() { |
711 | + RegionConnection.state = RegionConnection.STATE.UP; |
712 | + for(let i = 0; i < RegionConnection.maxMissedPings ; i++) { |
713 | + RegionConnection.pingsInFlight.add(i); |
714 | + } |
715 | + RegionConnection.ensureConnection(); |
716 | + expect(RegionConnection.retry).toHaveBeenCalled(); |
717 | + }); |
718 | + |
719 | + it("calls retry() if UP with missed requests and pings", function() { |
720 | + RegionConnection.state = RegionConnection.STATE.UP; |
721 | + RegionConnection.pingsInFlight.add(1); |
722 | + for(let i = 0; i < RegionConnection.maxPatience - 1 ; i++) { |
723 | + RegionConnection.requests[i] = {}; |
724 | + } |
725 | + RegionConnection.ensureConnection(); |
726 | + expect(RegionConnection.retry).toHaveBeenCalled(); |
727 | + }); |
728 | + |
729 | + it("calls ping() if in UP state", function() { |
730 | + RegionConnection.state = RegionConnection.STATE.UP; |
731 | + RegionConnection.ensureConnection(); |
732 | + expect(RegionConnection.ping).toHaveBeenCalled(); |
733 | + }); |
734 | + |
735 | + it("calls retry() and connect() if in RETRY state", function() { |
736 | + RegionConnection.state = RegionConnection.STATE.RETRY; |
737 | + RegionConnection.ensureConnection(); |
738 | + expect(RegionConnection.retry).toHaveBeenCalled(); |
739 | + expect(RegionConnection.connect).toHaveBeenCalled(); |
740 | + }); |
741 | + |
742 | + }); |
743 | + |
744 | describe("connect", function() { |
745 | |
746 | var url = "http://test-url", buildUrlSpy; |
747 | @@ -210,12 +286,6 @@ describe("RegionConnection", function() { |
748 | expect(RegionConnection.url).toBe(url); |
749 | }); |
750 | |
751 | - it("sets autoReconnect to true", function() { |
752 | - RegionConnection.autoReconnect = false; |
753 | - RegionConnection.connect(); |
754 | - expect(RegionConnection.autoReconnect).toBe(true); |
755 | - }); |
756 | - |
757 | it("calls buildSocket with url", function() { |
758 | RegionConnection.connect(); |
759 | expect(RegionConnection.buildSocket).toHaveBeenCalledWith(url); |
760 | @@ -231,7 +301,7 @@ describe("RegionConnection", function() { |
761 | it("sets connect to true when onopen called", function() { |
762 | RegionConnection.connect(); |
763 | webSocket.onopen({}); |
764 | - expect(RegionConnection.connected).toBe(true); |
765 | + expect(RegionConnection.isConnected()).toBe(true); |
766 | }); |
767 | |
768 | it("calls error handler when onerror called", function(done) { |
769 | @@ -244,39 +314,20 @@ describe("RegionConnection", function() { |
770 | webSocket.onerror(evt_obj); |
771 | }); |
772 | |
773 | - it("sets connect to false when onclose called", function() { |
774 | - RegionConnection.autoReconnect = false; |
775 | + it("isConnected() is false when onclose called", function() { |
776 | RegionConnection.connect(); |
777 | webSocket.onclose({}); |
778 | - expect(RegionConnection.connected).toBe(false); |
779 | + expect(RegionConnection.isConnected()).toBe(false); |
780 | }); |
781 | |
782 | it("calls close handler when onclose called", function(done) { |
783 | var evt_obj = {}; |
784 | - RegionConnection.autoReconnect = false; |
785 | - RegionConnection.connect(); |
786 | RegionConnection.registerHandler("close", function(evt) { |
787 | expect(evt).toBe(evt_obj); |
788 | done(); |
789 | }); |
790 | - webSocket.onclose(evt_obj); |
791 | - }); |
792 | - |
793 | - it("onclose calls connect when autoReconnect is true", function() { |
794 | - RegionConnection.connect(); |
795 | - var new_url = "http://new-url"; |
796 | - buildUrlSpy.and.returnValue(new_url); |
797 | - webSocket.onclose({}); |
798 | - $timeout.flush(); |
799 | - expect(RegionConnection.url).toBe(new_url); |
800 | - }); |
801 | - |
802 | - it("onclose sets error", function() { |
803 | RegionConnection.connect(); |
804 | - webSocket.onclose(); |
805 | - $timeout.flush(); |
806 | - expect(RegionConnection.error).toBe( |
807 | - "Unable to connect to: " + url); |
808 | + webSocket.onclose(evt_obj); |
809 | }); |
810 | |
811 | it("calls onMessage when onmessage called", function() { |
812 | @@ -289,29 +340,38 @@ describe("RegionConnection", function() { |
813 | }); |
814 | }); |
815 | |
816 | - describe("close", function() { |
817 | + describe("retry", function() { |
818 | |
819 | beforeEach(function() { |
820 | spyOn(webSocket, "close"); |
821 | }); |
822 | |
823 | - it("sets autoReconnect to false", function() { |
824 | + it("sets state to RETRY", function() { |
825 | RegionConnection.connect(""); |
826 | - RegionConnection.close(); |
827 | - expect(RegionConnection.autoReconnect).toBe(false); |
828 | + RegionConnection.retry(); |
829 | + expect(RegionConnection.state).toBe(RegionConnection.STATE.RETRY); |
830 | }); |
831 | |
832 | it("calls close on websocket", function() { |
833 | RegionConnection.connect(""); |
834 | - RegionConnection.close(); |
835 | + RegionConnection.retry(); |
836 | expect(webSocket.close).toHaveBeenCalled(); |
837 | }); |
838 | |
839 | it("sets websocket to null", function() { |
840 | RegionConnection.connect(""); |
841 | - RegionConnection.close(); |
842 | + RegionConnection.retry(); |
843 | expect(RegionConnection.websocket).toBeNull(); |
844 | }); |
845 | + |
846 | + it("clears out event handlers", function() { |
847 | + RegionConnection.connect(""); |
848 | + let ws = RegionConnection.websocket; |
849 | + RegionConnection.retry(); |
850 | + expect(ws.onopen).toBeNull(); |
851 | + expect(ws.onerror).toBeNull(); |
852 | + expect(ws.onclose).toBeNull(); |
853 | + }); |
854 | }); |
855 | |
856 | describe("_getProtocol", function() { |
857 | @@ -413,7 +473,7 @@ describe("RegionConnection", function() { |
858 | describe("defaultConnect", function() { |
859 | |
860 | it("resolve defer if already connected", function(done) { |
861 | - RegionConnection.connected = true; |
862 | + RegionConnection.state = RegionConnection.STATE.UP; |
863 | RegionConnection.defaultConnect().then(function() { |
864 | done(); |
865 | }); |
866 | diff --git a/src/maasserver/static/js/angular/services/log.js b/src/maasserver/static/js/angular/services/log.js |
867 | index f2378ab..894ae03 100644 |
868 | --- a/src/maasserver/static/js/angular/services/log.js |
869 | +++ b/src/maasserver/static/js/angular/services/log.js |
870 | @@ -51,7 +51,7 @@ angular.module('MAAS').service('LogService', [ |
871 | // Add time index to the beginning of the log. |
872 | Array.prototype.unshift.call(args, |
873 | "[" + self.formatMilliseconds(self.now()) + "]"); |
874 | - destination.apply(self, args); |
875 | + destination.apply(console, args); |
876 | } |
877 | }; |
878 | |
879 | diff --git a/src/maasserver/static/js/angular/testing/websocket.js b/src/maasserver/static/js/angular/testing/websocket.js |
880 | index f32fd42..54aeeb1 100644 |
881 | --- a/src/maasserver/static/js/angular/testing/websocket.js |
882 | +++ b/src/maasserver/static/js/angular/testing/websocket.js |
883 | @@ -13,9 +13,13 @@ var READY_STATES = { |
884 | CLOSED: 3 |
885 | }; |
886 | |
887 | -var MSG_TYPE = { |
888 | +// Message types (copied from region.js) |
889 | +const MSG_TYPE = { |
890 | REQUEST: 0, |
891 | - RESPONSE: 1 |
892 | + RESPONSE: 1, |
893 | + NOTIFY: 2, |
894 | + PING: 3, |
895 | + PING_REPLY: 4 |
896 | }; |
897 | |
898 | function MockWebSocket(url) { |
899 | @@ -26,12 +30,16 @@ function MockWebSocket(url) { |
900 | this.onmessage = null; |
901 | this.onopen = null; |
902 | |
903 | + this.replyToPing = true; |
904 | + |
905 | this.sentData = []; |
906 | this.returnData = []; |
907 | this.receivedData = []; |
908 | |
909 | + this.sequenceNum = 0; |
910 | + |
911 | // Simulate the connection opening. |
912 | - var self = this; |
913 | + let self = this; |
914 | setTimeout(function() { |
915 | self.readyState = READY_STATES.OPEN; |
916 | if(angular.isFunction(self.onopen)) { |
917 | @@ -44,7 +52,7 @@ MockWebSocket.prototype.close = function() { |
918 | this.readyState = READY_STATES.CLOSING; |
919 | |
920 | // Simulate the connection closing. |
921 | - var self = this; |
922 | + let self = this; |
923 | setTimeout(function() { |
924 | self.readyState = READY_STATES.CLOSED; |
925 | if(angular.isFunction(self.onclose)) { |
926 | @@ -54,6 +62,26 @@ MockWebSocket.prototype.close = function() { |
927 | }; |
928 | |
929 | MockWebSocket.prototype.send = function(data) { |
930 | + let sentObject = angular.fromJson(data); |
931 | + let sentType = sentObject.type; |
932 | + let sentId = sentObject.request_id; |
933 | + let self = this; |
934 | + |
935 | + // Special case for PING type; just reply and return. |
936 | + if(self.replyToPing && angular.isNumber(sentType) && |
937 | + sentType === MSG_TYPE.PING) { |
938 | + setTimeout(function() { |
939 | + self.sequenceNum += 1; |
940 | + let pingResultData = angular.toJson({ |
941 | + type: MSG_TYPE.PING_REPLY, |
942 | + request_id: sentId, |
943 | + result: self.sequenceNum |
944 | + }); |
945 | + self.onmessage({ data: pingResultData }); |
946 | + }); |
947 | + return; |
948 | + } |
949 | + |
950 | this.sentData.push(data); |
951 | |
952 | // Exit early if no fake data to return. |
953 | @@ -68,13 +96,9 @@ MockWebSocket.prototype.send = function(data) { |
954 | receivedData = [receivedData]; |
955 | } |
956 | |
957 | - var self = this; |
958 | |
959 | // Send the response |
960 | setTimeout(function() { |
961 | - var sentObject = angular.fromJson(data); |
962 | - var sentType = sentObject.type; |
963 | - var sentId = sentObject.request_id; |
964 | if(angular.isNumber(sentType) && angular.isNumber(sentId)) { |
965 | // Patch the request_id so the response is the |
966 | // same as the request. |
967 | diff --git a/webpack.config.js b/webpack.config.js |
968 | index 9880aff..b02d7ce 100644 |
969 | --- a/webpack.config.js |
970 | +++ b/webpack.config.js |
971 | @@ -27,6 +27,7 @@ module.exports = { |
972 | plugins: [ |
973 | new UglifyJSPlugin({ |
974 | parallel: true, |
975 | + cache: true, |
976 | sourceMap: true, |
977 | uglifyOptions: { |
978 | // Using the 'mangle' option breaks the Angular injector. |
UNIT TESTS
-b enable-ping--js--bug-1802325 lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: SUCCESS 1206dab15cf455c ca55ce3430
COMMIT: a354b3b71ad8256