add support of cancellation of queued build

This commit is contained in:
oleg 2015-11-25 01:41:20 +03:00
parent 55e5b257e6
commit 5f9b61bcb0
10 changed files with 196 additions and 8 deletions

View File

@ -67,4 +67,4 @@ problems detected by regexp) then step will be rerun without error
* Ability to change build parameters from ui (at least target branch)
* ~~Embedded database (apparently level db)~~
* ~~Lightweight (minimal dependencies)~~
* Cancel build
* ~~Cancel build~~

View File

@ -14,7 +14,7 @@ http:
storage:
backend: memdown
# backend: medeadown
# backend: leveldown
notify:
mail:

View File

@ -33,6 +33,14 @@ exports.init = function(app, callback) {
},
callback
);
},
removeBuild: function(build, callback) {
Steppy(
function() {
db.builds.del([build.id], this.slot());
},
callback
);
}
});
@ -72,9 +80,9 @@ exports.init = function(app, callback) {
exports.createBuildDataResource = createBuildDataResource;
distributor.on('buildUpdate', function(build, changes) {
var buildsResource = app.dataio.resource('builds');
var buildsResource = app.dataio.resource('builds');
distributor.on('buildUpdate', function(build, changes) {
if (build.status === 'queued') {
createBuildDataResource(build.id);
}
@ -91,6 +99,10 @@ exports.init = function(app, callback) {
});
});
distributor.on('buildCancel', function(build) {
buildsResource.clientEmitSync('cancel', {buildId: build.id});
});
var buildLogLineNumbersHash = {};
distributor.on('buildData', function(build, data) {

View File

@ -22,6 +22,10 @@ function Distributor(params) {
callback(null, build);
};
self.removeBuild = params.removeBuild || function(build, callback) {
callback();
};
self.projects = params.projects;
}
@ -209,6 +213,36 @@ Distributor.prototype._updateBuild = function(build, changes, callback) {
);
};
Distributor.prototype.cancel = function(params, callback) {
var self = this;
Steppy(
function() {
var queueItemIndex = _(self.queue).findIndex(function(item) {
return item.build.id === params.buildId;
});
if (queueItemIndex === -1) {
throw new Error(
'Build with id "' + params.buildId + '" not found for cancel'
);
}
// only queued build are in the queue, so there is no reason
// to check status
var build = self.queue[queueItemIndex].build;
// remove from queue
self.queue.splice(queueItemIndex, 1)[0];
// remove from db
self.removeBuild(build, this.slot());
self.emit('buildCancel', build);
},
callback
);
};
Distributor.prototype.run = function(params, callback) {
var self = this,
project;

View File

@ -3,10 +3,12 @@
var Steppy = require('twostep').Steppy,
_ = require('underscore'),
db = require('../db'),
utils = require('../lib/utils');
utils = require('../lib/utils'),
logger = require('../lib/logger')('builds resource');
module.exports = function(app) {
var resource = app.dataio.resource('builds');
var resource = app.dataio.resource('builds'),
distributor = app.distributor;
resource.use('readAll', function(req, res, next) {
Steppy(
@ -104,5 +106,19 @@ module.exports = function(app) {
);
});
resource.use('cancel', function(req, res, next) {
Steppy(
function() {
var buildId = req.data.buildId;
logger.log('Cancel build: "%s"', buildId);
distributor.cancel({buildId: buildId}, this.slot());
},
function() {
res.send();
},
next
);
});
return resource;
};

View File

@ -2,6 +2,7 @@
define(['reflux'], function(Reflux) {
var Actions = Reflux.createActions([
'cancel',
'readTerminalOutput',
'readAll',
'read'

View File

@ -84,4 +84,10 @@ mixin statusText(build)
.build_controls_progress
if build.project.avgBuildDuration
Progress(build=build)
if build.status === 'queued'
.build_controls_buttons
a.btn.btn-sm.btn-default(href="javascript:void(0);", onClick=this.onCancelBuild(build.id))
i.fa.fa-fw.fa-times(title="Cancel build")
|
| Cancel build

View File

@ -30,6 +30,9 @@ define([
onRebuildProject: function(projectName) {
ProjectActions.run(projectName)
},
onCancelBuild: function(buildId) {
BuildActions.cancel(buildId);
},
onShowTerminal: function(build) {
this.setState({showTerminal: !this.state.showTerminal});
BuildActions.readTerminalOutput(this.props.build);

View File

@ -14,7 +14,7 @@ define([
return this.builds;
},
onChange: function(data, action) {
onChanged: function(data) {
var oldBuild = _(this.builds).findWhere({id: data.buildId});
if (oldBuild) {
_(oldBuild).extend(data.changes);
@ -27,8 +27,23 @@ define([
this.trigger(this.builds);
},
onCancelled: function(data) {
// WORKAROUND: client that trigger `onCancel` gets one `onCancelled`
// call other clients get 2 calls (second with empty data)
if (!data) {
return;
}
var index = _(this.builds).findIndex({id: data.buildId});
if (index !== -1) {
this.builds.splice(index, 1);
}
this.trigger(this.builds);
},
init: function() {
resource.subscribe('change', this.onChange);
resource.subscribe('change', this.onChanged);
resource.subscribe('cancel', this.onCancelled);
},
onReadAll: function(params) {
@ -39,6 +54,12 @@ define([
self.trigger(self.builds);
});
},
onCancel: function(buildId) {
resource.sync('cancel', {buildId: buildId}, function(err) {
if (err) throw err;
});
}
});
return Store;

View File

@ -128,4 +128,99 @@ describe('Distributor main', function() {
Distributor.prototype._createNode.restore();
});
});
describe('with success project and build cancel', function() {
before(function() {
sinon.stub(Distributor.prototype, '_createNode', createNodeMock(
sinon.stub().callsArgAsync(1)
));
});
var distributorParams = {
projects: projects,
nodes: [{type: 'local', maxExecutorsCount: 1}],
saveBuild: function(build, callback) {
build.id = 1;
callback(null, build);
}
};
describe('when cancel queued buid', function() {
var updateBuildSpy;
var cancelError;
it('instance should be created without errors', function() {
distributor = new Distributor(distributorParams);
var originalRunNext = distributor._runNext;
distributor._runNext = function() {
distributor.cancel({buildId: 1}, function(err) {
cancelError = err;
});
originalRunNext.apply(distributor, arguments);
};
updateBuildSpy = sinon.spy(distributor, '_updateBuild');
});
it('should run without errors', function(done) {
distributor.run({projectName: 'project1'}, function(err) {
expect(err).not.ok();
done();
});
});
it('build should be queued', function() {
var changes = updateBuildSpy.getCall(0).args[1];
expect(changes).only.have.keys(
'project', 'initiator', 'params', 'createDate', 'status',
'completed'
);
expect(changes.status).equal('queued');
expect(changes.completed).equal(false);
});
it('should be cancelled without error', function() {
expect(cancelError).not.ok();
});
it('update build called only once', function() {
expect(updateBuildSpy.callCount).equal(1);
});
});
describe('when try to cancel unexisted build', function() {
var cancelError;
it('instance should be created without errors', function() {
distributor = new Distributor(distributorParams);
var originalRunNext = distributor._runNext;
distributor._runNext = function() {
distributor.cancel({buildId: 2}, function(err) {
cancelError = err;
});
originalRunNext.apply(distributor, arguments);
};
});
it('should run without errors', function(done) {
distributor.run({projectName: 'project1'}, function(err) {
expect(err).not.ok();
done();
});
});
it('should be cancelled with error (build not found)', function() {
expect(cancelError).ok();
expect(cancelError.message).eql(
'Build with id "2" not found for cancel'
);
});
});
after(function() {
Distributor.prototype._createNode.restore();
});
});
});