Skip to content

Commit

Permalink
Feature: Unstable connection support.
Browse files Browse the repository at this point in the history
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
jan-molak committed Dec 18, 2013
1 parent 311b92c commit e4528af
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
</script>

<script src="${resourcesURL}/scripts/app.js"></script>
<script src="${resourcesURL}/scripts/cron.js"></script>
<script src="${resourcesURL}/scripts/services.js"></script>
<script src="${resourcesURL}/scripts/jenkins.js"></script>
<script src="${resourcesURL}/scripts/controllers.js"></script>
Expand Down
4 changes: 0 additions & 4 deletions src/main/webapp/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,4 @@ angular.
if (! Modernizr.flexbox) {
notifyUser.aboutInsufficientSupportOfCSS3('flexbox');
}

$rootScope.$on('communication-error', function(event, error) {
notifyUser.about(error.status);
});
});
102 changes: 76 additions & 26 deletions src/main/webapp/scripts/controllers.js
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);
});
}]);
42 changes: 42 additions & 0 deletions src/main/webapp/scripts/cron.js
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;
}];
});
205 changes: 205 additions & 0 deletions src/test/javascript/unit/cron/everySpec.js
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);
}));
});
});
});
});
1 change: 1 addition & 0 deletions src/test/resources/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = function(config) {
'src/main/webapp/vendor/ui-*.js',
'src/test/resources/vendor/angular-mocks.js',
'src/test/resources/vendor/sinon-1.7.3.js',
'src/test/resources/vendor/jasmine-sinon-0.3.1.js',
'src/test/resources/vendor/yahoo-2.9.0.min.js',
'src/test/resources/vendor/yahoo-cookie-2.9.0.min.js',
'src/main/webapp/scripts/**/*.js',
Expand Down
Loading

0 comments on commit e4528af

Please sign in to comment.