-
-
Notifications
You must be signed in to change notification settings - Fork 279
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Unstable connection support.
Build Monitor: - tries to re-establish connectivity with Jenkins mother ship automatically when the connection drops - detects when Jenkins gets restarted and reloads itself automatically Resolves #21 and #32
- Loading branch information
Showing
7 changed files
with
383 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,81 @@ | ||
'use strict'; | ||
|
||
angular. | ||
module('buildMonitor.controllers', [ 'buildMonitor.services', 'uiSlider']). | ||
|
||
controller('JobViews', function($scope, $rootScope, $dialog, $timeout, proxy, cookieJar) { | ||
$scope.fontSize = cookieJar.get('fontSize', 1); | ||
$scope.numberOfColumns = cookieJar.get('numberOfColumns', 2); | ||
|
||
$scope.$watch('fontSize', function(currentFontSize) { | ||
cookieJar.put('fontSize', currentFontSize); | ||
}); | ||
$scope.$watch('numberOfColumns', function(currentNumberOfColumns) { | ||
cookieJar.put('numberOfColumns', currentNumberOfColumns); | ||
}); | ||
|
||
|
||
$scope.jobs = {}; | ||
var update = function() { | ||
var updating; | ||
proxy.buildMonitor.fetchJobViews().then(function(response) { | ||
$scope.jobs = response.data.jobs; | ||
updating = $timeout(update, 5000); | ||
}, function(error) { | ||
$timeout.cancel(updating); | ||
$rootScope.$broadcast("communication-error", error); | ||
module('buildMonitor.controllers', [ 'buildMonitor.services', 'buildMonitor.cron', 'uiSlider']). | ||
|
||
controller('JobViews', ['$scope', '$rootScope', 'proxy', 'cookieJar', 'every', 'connectionErrorHandler', | ||
function ($scope, $rootScope, proxy, cookieJar, every, connectionErrorHandler) { | ||
|
||
// todo: consider extracting a Configuration Controller | ||
$scope.fontSize = cookieJar.get('fontSize', 1); | ||
$scope.numberOfColumns = cookieJar.get('numberOfColumns', 2); | ||
|
||
$scope.$watch('fontSize', function (currentFontSize) { | ||
cookieJar.put('fontSize', currentFontSize); | ||
}); | ||
$scope.$watch('numberOfColumns', function (currentNumberOfColumns) { | ||
cookieJar.put('numberOfColumns', currentNumberOfColumns); | ||
}); | ||
|
||
// | ||
|
||
var handleErrorAndDecideOnNext = connectionErrorHandler.handleErrorAndNotify, | ||
fetchJobViews = proxy.buildMonitor.fetchJobViews; | ||
|
||
$scope.jobs = {}; | ||
|
||
every(5000, function (step) { | ||
|
||
fetchJobViews().then(function (response) { | ||
|
||
$scope.jobs = response.data.jobs | ||
step.resolve(); | ||
|
||
}, handleErrorAndDecideOnNext(step)); | ||
}); | ||
} | ||
}]). | ||
|
||
service('connectionErrorHandler', ['$rootScope', | ||
function ($rootScope) { | ||
this.handleErrorAndNotify = function (deferred) { | ||
|
||
function handleLostConnection(error) { | ||
deferred.resolve(); | ||
$rootScope.$broadcast("jenkins:connection-lost", error); | ||
} | ||
|
||
function handleJenkinsRestart(error) { | ||
deferred.reject(); | ||
$rootScope.$broadcast("jenkins:restarted", error); | ||
} | ||
|
||
function handleUnknown(error) { | ||
deferred.reject(); | ||
$rootScope.$broadcast("jenkins:unknown-communication-error", error); | ||
} | ||
|
||
return function (error) { | ||
switch (error.status) { | ||
case 0: handleLostConnection(error); break; | ||
case 404: handleJenkinsRestart(error); break; | ||
default: handleUnknown(error); break; | ||
} | ||
} | ||
} | ||
}]). | ||
|
||
update(); | ||
}); | ||
run(['$rootScope', '$window', '$log', 'notifyUser', | ||
function ($rootScope, $window, $log, notifyUser) { | ||
$rootScope.$on('jenkins:connection-lost', function (event, error) { | ||
// todo: notify the user about the problem and what we're doing in order to resolve it | ||
$log.info('Connection with Jenkins mother ship is lost. I\'ll try to reconnect in a couple of seconds and see if we have more luck...'); | ||
}); | ||
|
||
$rootScope.$on('jenkins:restarted', function (event, error) { | ||
$window.location.reload(); | ||
}); | ||
|
||
$rootScope.$on('jenkins:unknown-communication-error', function (event, error) { | ||
notifyUser.about(error.status); | ||
}); | ||
}]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
angular. | ||
module('buildMonitor.cron', []). | ||
|
||
provider('every', function () { | ||
this.$get = ['$rootScope', '$window', '$q', '$timeout', | ||
function ($rootScope, $window, $q, $timeout) { | ||
|
||
function every(interval, command) { | ||
var applyRootScope = function() { | ||
$rootScope.$$phase || $rootScope.$apply(); | ||
}; | ||
|
||
function synchronous(command) { | ||
command(); | ||
$timeout(step, interval); | ||
} | ||
|
||
function asynchronous(command) { | ||
var deferred = $q.defer(), | ||
promise = deferred.promise; | ||
|
||
promise.then(function() { | ||
$timeout(step, interval); | ||
}); | ||
|
||
command(deferred); | ||
} | ||
|
||
function step() { | ||
(command.length == 0) | ||
? synchronous(command) | ||
: asynchronous(command); | ||
|
||
applyRootScope(); | ||
} | ||
|
||
step(); | ||
} | ||
|
||
return every; | ||
}]; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
describe('buildMonitor', function () { | ||
describe('buildMonitor.cron', function () { | ||
describe('every', function () { | ||
|
||
var interval = 1000; | ||
|
||
var noop = angular.noop; | ||
|
||
beforeEach(module('buildMonitor.cron', ['everyProvider', '$provide', function (everyProvider, $provide) { | ||
|
||
var timeline = [], | ||
nextId = 0, | ||
now = 0, | ||
$window, | ||
$timeout; | ||
|
||
$timeout = function (fn, delay) { | ||
timeline.push({ | ||
nextTime: (now + delay), | ||
delay: delay, | ||
fn: fn, | ||
id: nextId | ||
}); | ||
timeline.sort(function (a, b) { | ||
return a.nextTime - b.nextTime; | ||
}); | ||
|
||
return nextId++; | ||
}; | ||
|
||
$timeout.flush = function (millis) { | ||
now += millis; | ||
while (timeline.length && timeline[0].nextTime <= now) { | ||
var task = timeline[0]; | ||
task.fn(); | ||
|
||
clearTimeout(task.id) | ||
|
||
timeline.sort(function (a, b) { | ||
return a.nextTime - b.nextTime; | ||
}); | ||
} | ||
|
||
return millis; | ||
|
||
|
||
function clearTimeout(id) { | ||
var fnIndex; | ||
|
||
angular.forEach(timeline, function (fn, index) { | ||
if (fn.id === id) fnIndex = index; | ||
}); | ||
|
||
if (fnIndex !== undefined) { | ||
timeline.splice(fnIndex, 1); | ||
return true; | ||
} | ||
|
||
return false; | ||
}; | ||
}; | ||
|
||
$provide.provider('every', everyProvider); | ||
$provide.value('$timeout', $timeout); | ||
}])); | ||
|
||
it('executes the first iteration straight away', inject(function (every) { | ||
var counter = 0; | ||
|
||
every(interval, function () { | ||
counter++; | ||
}); | ||
|
||
expect(counter).toBe(1); | ||
})); | ||
|
||
it('runs task repeatedly', inject(function (every, $timeout) { | ||
var counter = 0; | ||
|
||
every(interval, function () { | ||
counter++; | ||
}); | ||
|
||
$timeout.flush(interval); | ||
|
||
expect(counter).toBe(2); | ||
})); | ||
|
||
it('calls $apply after each task is executed', inject(function (every, $rootScope) { | ||
var apply = sinon.spy($rootScope, '$apply'); | ||
|
||
every(interval, noop); | ||
|
||
expect(apply).toHaveBeenCalledOnce(); | ||
})); | ||
|
||
describe('asynchronous steps', function () { | ||
|
||
it('shows how angular $q needs to be used with $rootScope.$digest', inject(function ($q, $rootScope) { | ||
// https://groups.google.com/forum/#!topic/angular/0dhQzTPexA0 | ||
|
||
var deferred = $q.defer(), | ||
promise = deferred.promise, | ||
counter = 0; | ||
|
||
promise.then(function () { | ||
counter++; | ||
}); | ||
|
||
deferred.resolve(); | ||
|
||
expect(counter).toBe(0); | ||
|
||
// that's the important bit ! | ||
$rootScope.$digest(); | ||
|
||
expect(counter).toBe(1); | ||
})); | ||
|
||
it('progresses the loop if the current step resolves the promise of the next step', inject(function (every, $rootScope, $timeout, $q) { | ||
var task = sinon.spy(); | ||
|
||
every(interval, function (deferred) { | ||
task(); | ||
|
||
deferred.resolve(); | ||
|
||
return deferred; | ||
}); | ||
|
||
|
||
expect(task.callCount).toBe(1); | ||
|
||
$rootScope.$digest(); | ||
$timeout.flush(interval); | ||
|
||
expect(task.callCount).toBe(2); | ||
})); | ||
|
||
it('stops the loop if the current step brakes the promise of a next step', inject(function (every, $rootScope, $timeout) { | ||
var task = sinon.spy(); | ||
|
||
every(interval, function (deferred) { | ||
task(); | ||
|
||
deferred.reject(); | ||
|
||
return deferred; | ||
}); | ||
|
||
$timeout.flush(interval) | ||
|
||
expect(task.callCount).toBe(1); | ||
})); | ||
|
||
|
||
it('calls $apply after the step is completed', inject(function (every, $rootScope) { | ||
var apply = sinon.spy($rootScope, '$apply'); | ||
|
||
every(interval, function (deferred) { | ||
deferred.resolve(); | ||
|
||
return deferred; | ||
}); | ||
|
||
expect(apply.callCount).toBe(1); | ||
})); | ||
|
||
it("won't run the next step until the previous step has completed", inject(function (every, $q, $timeout, $rootScope) { | ||
var syncTask = sinon.spy(), | ||
|
||
asyncTask = sinon.spy(), | ||
asyncTaskLength = 2 * interval; | ||
|
||
every(interval, function (deferred) { | ||
syncTask(); | ||
|
||
$timeout(function () { | ||
asyncTask(); | ||
|
||
deferred.resolve(); | ||
|
||
}, asyncTaskLength); | ||
|
||
return deferred; | ||
}); | ||
|
||
// straight after invoking 'every': | ||
expect(syncTask.callCount).toBe(1); | ||
expect(asyncTask.callCount).toBe(0); | ||
|
||
// after the interval (half the time it takes to complete the asyncTask): | ||
$timeout.flush(interval); | ||
expect(syncTask.callCount).toBe(1); | ||
expect(asyncTask.callCount).toBe(0); | ||
|
||
// after another interval (it takes two intervals to complete the asyncTask): | ||
$timeout.flush(interval); | ||
expect(syncTask.callCount).toBe(1); | ||
expect(asyncTask.callCount).toBe(1); | ||
})); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.