mirror of
https://gitlab.silvrtree.co.uk/martind2000/nci.git
synced 2025-02-11 03:39:15 +00:00
add support of cancellation of queued build
This commit is contained in:
parent
55e5b257e6
commit
5f9b61bcb0
@ -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)
|
* Ability to change build parameters from ui (at least target branch)
|
||||||
* ~~Embedded database (apparently level db)~~
|
* ~~Embedded database (apparently level db)~~
|
||||||
* ~~Lightweight (minimal dependencies)~~
|
* ~~Lightweight (minimal dependencies)~~
|
||||||
* Cancel build
|
* ~~Cancel build~~
|
||||||
|
@ -14,7 +14,7 @@ http:
|
|||||||
|
|
||||||
storage:
|
storage:
|
||||||
backend: memdown
|
backend: memdown
|
||||||
# backend: medeadown
|
# backend: leveldown
|
||||||
|
|
||||||
notify:
|
notify:
|
||||||
mail:
|
mail:
|
||||||
|
@ -33,6 +33,14 @@ exports.init = function(app, callback) {
|
|||||||
},
|
},
|
||||||
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;
|
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') {
|
if (build.status === 'queued') {
|
||||||
createBuildDataResource(build.id);
|
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 = {};
|
var buildLogLineNumbersHash = {};
|
||||||
|
|
||||||
distributor.on('buildData', function(build, data) {
|
distributor.on('buildData', function(build, data) {
|
||||||
|
@ -22,6 +22,10 @@ function Distributor(params) {
|
|||||||
callback(null, build);
|
callback(null, build);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.removeBuild = params.removeBuild || function(build, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
self.projects = params.projects;
|
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) {
|
Distributor.prototype.run = function(params, callback) {
|
||||||
var self = this,
|
var self = this,
|
||||||
project;
|
project;
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
var Steppy = require('twostep').Steppy,
|
var Steppy = require('twostep').Steppy,
|
||||||
_ = require('underscore'),
|
_ = require('underscore'),
|
||||||
db = require('../db'),
|
db = require('../db'),
|
||||||
utils = require('../lib/utils');
|
utils = require('../lib/utils'),
|
||||||
|
logger = require('../lib/logger')('builds resource');
|
||||||
|
|
||||||
module.exports = function(app) {
|
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) {
|
resource.use('readAll', function(req, res, next) {
|
||||||
Steppy(
|
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;
|
return resource;
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
define(['reflux'], function(Reflux) {
|
define(['reflux'], function(Reflux) {
|
||||||
var Actions = Reflux.createActions([
|
var Actions = Reflux.createActions([
|
||||||
|
'cancel',
|
||||||
'readTerminalOutput',
|
'readTerminalOutput',
|
||||||
'readAll',
|
'readAll',
|
||||||
'read'
|
'read'
|
||||||
|
@ -84,4 +84,10 @@ mixin statusText(build)
|
|||||||
.build_controls_progress
|
.build_controls_progress
|
||||||
if build.project.avgBuildDuration
|
if build.project.avgBuildDuration
|
||||||
Progress(build=build)
|
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
|
||||||
|
|
||||||
|
@ -30,6 +30,9 @@ define([
|
|||||||
onRebuildProject: function(projectName) {
|
onRebuildProject: function(projectName) {
|
||||||
ProjectActions.run(projectName)
|
ProjectActions.run(projectName)
|
||||||
},
|
},
|
||||||
|
onCancelBuild: function(buildId) {
|
||||||
|
BuildActions.cancel(buildId);
|
||||||
|
},
|
||||||
onShowTerminal: function(build) {
|
onShowTerminal: function(build) {
|
||||||
this.setState({showTerminal: !this.state.showTerminal});
|
this.setState({showTerminal: !this.state.showTerminal});
|
||||||
BuildActions.readTerminalOutput(this.props.build);
|
BuildActions.readTerminalOutput(this.props.build);
|
||||||
|
@ -14,7 +14,7 @@ define([
|
|||||||
return this.builds;
|
return this.builds;
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange: function(data, action) {
|
onChanged: function(data) {
|
||||||
var oldBuild = _(this.builds).findWhere({id: data.buildId});
|
var oldBuild = _(this.builds).findWhere({id: data.buildId});
|
||||||
if (oldBuild) {
|
if (oldBuild) {
|
||||||
_(oldBuild).extend(data.changes);
|
_(oldBuild).extend(data.changes);
|
||||||
@ -27,8 +27,23 @@ define([
|
|||||||
this.trigger(this.builds);
|
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() {
|
init: function() {
|
||||||
resource.subscribe('change', this.onChange);
|
resource.subscribe('change', this.onChanged);
|
||||||
|
resource.subscribe('cancel', this.onCancelled);
|
||||||
},
|
},
|
||||||
|
|
||||||
onReadAll: function(params) {
|
onReadAll: function(params) {
|
||||||
@ -39,6 +54,12 @@ define([
|
|||||||
self.trigger(self.builds);
|
self.trigger(self.builds);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onCancel: function(buildId) {
|
||||||
|
resource.sync('cancel', {buildId: buildId}, function(err) {
|
||||||
|
if (err) throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Store;
|
return Store;
|
||||||
|
@ -128,4 +128,99 @@ describe('Distributor main', function() {
|
|||||||
Distributor.prototype._createNode.restore();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user