2015-04-09 18:55:29 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var Steppy = require('twostep').Steppy,
|
|
|
|
_ = require('underscore'),
|
2015-05-10 10:04:54 +00:00
|
|
|
Node = require('./node').Node,
|
|
|
|
EventEmitter = require('events').EventEmitter,
|
2015-05-21 21:09:16 +00:00
|
|
|
inherits = require('util').inherits,
|
2015-07-05 18:03:58 +00:00
|
|
|
logger = require('./logger')('distributor');
|
2015-04-09 18:55:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
function Distributor(params) {
|
|
|
|
var self = this;
|
|
|
|
// nodes to execute builds
|
|
|
|
self.nodes = _(params.nodes).map(function(nodeParams) {
|
|
|
|
return self._createNode(nodeParams);
|
|
|
|
});
|
|
|
|
// queued projects to build
|
|
|
|
self.queue = [];
|
2015-04-09 19:39:22 +00:00
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
self.saveBuild = params.saveBuild || function(build, callback) {
|
2015-05-01 11:11:29 +00:00
|
|
|
callback(null, build);
|
2015-04-09 19:39:22 +00:00
|
|
|
};
|
2015-06-13 18:59:32 +00:00
|
|
|
|
2015-11-24 22:41:20 +00:00
|
|
|
self.removeBuild = params.removeBuild || function(build, callback) {
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
2015-06-14 13:40:20 +00:00
|
|
|
self.projects = params.projects;
|
2016-01-10 16:37:19 +00:00
|
|
|
self.notifier = params.notifier;
|
2016-01-06 21:11:14 +00:00
|
|
|
|
|
|
|
self.buildLogLineNumbersHash = {},
|
|
|
|
self.lastLinesHash = {};
|
2015-04-09 18:55:29 +00:00
|
|
|
}
|
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
inherits(Distributor, EventEmitter);
|
|
|
|
|
2015-04-09 18:55:29 +00:00
|
|
|
exports.Distributor = Distributor;
|
|
|
|
|
|
|
|
Distributor.prototype._createNode = function(nodeParams) {
|
|
|
|
return new Node(nodeParams);
|
|
|
|
};
|
|
|
|
|
|
|
|
Distributor.prototype._runNext = function(callback) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
Steppy(
|
|
|
|
function() {
|
2015-06-14 23:27:58 +00:00
|
|
|
// update wait reasons for all queue items before run
|
|
|
|
self._updateWaitReasons();
|
|
|
|
|
2015-04-09 18:55:29 +00:00
|
|
|
var node;
|
|
|
|
var queueItemIndex = _(self.queue).findIndex(function(item) {
|
|
|
|
node = _(self.nodes).find(function(node) {
|
|
|
|
return node.hasFreeExecutor(item.project);
|
|
|
|
});
|
|
|
|
return node;
|
|
|
|
});
|
|
|
|
|
|
|
|
// quit if we have no suitable project
|
2015-07-08 21:19:33 +00:00
|
|
|
if (queueItemIndex === -1) {
|
2015-04-09 18:55:29 +00:00
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
|
2015-06-16 20:51:38 +00:00
|
|
|
var queueItem = self.queue.splice(queueItemIndex, 1)[0],
|
|
|
|
build = queueItem.build;
|
2015-04-09 18:55:29 +00:00
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
self._updateBuild(
|
2015-06-16 20:51:38 +00:00
|
|
|
build,
|
2015-06-15 18:40:01 +00:00
|
|
|
{startDate: Date.now(), status: 'in-progress', waitReason: ''},
|
2015-05-10 10:04:54 +00:00
|
|
|
this.slot()
|
|
|
|
);
|
2015-06-16 20:51:38 +00:00
|
|
|
|
2015-04-09 18:55:29 +00:00
|
|
|
var stepCallback = this.slot();
|
2015-06-17 20:04:10 +00:00
|
|
|
// run project on the first step two prevent parallel run next calls
|
2015-05-23 23:22:09 +00:00
|
|
|
var executor = node.run(build.project, build.params, function(err) {
|
2015-05-10 10:04:54 +00:00
|
|
|
self._updateBuild(
|
|
|
|
build,
|
|
|
|
{
|
|
|
|
endDate: Date.now(),
|
|
|
|
status: err ? 'error' : 'done',
|
2015-05-21 19:04:38 +00:00
|
|
|
completed: true,
|
2015-06-28 14:47:34 +00:00
|
|
|
error: err ? {
|
|
|
|
message: err.message, stderr: err.stderr
|
|
|
|
} : null
|
2015-05-10 10:04:54 +00:00
|
|
|
},
|
|
|
|
function(err, build) {
|
2015-06-15 18:40:01 +00:00
|
|
|
if (err) {
|
2015-07-05 18:03:58 +00:00
|
|
|
logger.error(
|
2015-06-15 18:40:01 +00:00
|
|
|
'Error during build update: ', err.stack || err
|
|
|
|
);
|
|
|
|
return stepCallback(err);
|
|
|
|
}
|
|
|
|
self._onBuildComplete(build, stepCallback);
|
2015-05-10 10:04:54 +00:00
|
|
|
}
|
|
|
|
);
|
2015-04-09 18:55:29 +00:00
|
|
|
});
|
2015-05-03 21:53:11 +00:00
|
|
|
|
2015-05-20 20:20:51 +00:00
|
|
|
executor.on('currentStep', function(stepName) {
|
|
|
|
self._updateBuild(build, {currentStep: stepName});
|
2015-05-05 23:11:28 +00:00
|
|
|
});
|
|
|
|
|
2015-07-11 21:27:02 +00:00
|
|
|
executor.on('stepTimingsChange', function(stepTimings) {
|
|
|
|
self._updateBuild(build, {stepTimings: stepTimings});
|
|
|
|
});
|
|
|
|
|
2015-05-03 21:53:11 +00:00
|
|
|
executor.on('data', function(data) {
|
2016-01-06 21:11:14 +00:00
|
|
|
self._onBuildData(build, data);
|
2015-05-03 21:53:11 +00:00
|
|
|
});
|
2015-05-09 16:59:27 +00:00
|
|
|
|
|
|
|
executor.once('scmData', function(scmData) {
|
2015-05-10 10:04:54 +00:00
|
|
|
self._updateBuild(build, {scm: scmData});
|
2015-06-24 21:54:33 +00:00
|
|
|
// run the same project again if we don't reach the latest rev
|
|
|
|
if (!scmData.isLatest) {
|
|
|
|
self.run({
|
|
|
|
projectName: build.project.name,
|
|
|
|
initiator: {
|
|
|
|
type: 'build',
|
|
|
|
id: build.id,
|
|
|
|
number: build.number,
|
|
|
|
project: {name: build.project.name}
|
2015-12-25 20:57:59 +00:00
|
|
|
}
|
2015-07-05 18:03:58 +00:00
|
|
|
});
|
2015-06-24 21:54:33 +00:00
|
|
|
}
|
2015-05-09 16:59:27 +00:00
|
|
|
});
|
2015-06-14 23:27:58 +00:00
|
|
|
|
|
|
|
// update wait reasons for all queue items after run
|
|
|
|
self._updateWaitReasons();
|
2015-04-09 18:55:29 +00:00
|
|
|
},
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2016-01-06 21:11:14 +00:00
|
|
|
Distributor.prototype._onBuildData = function(build, data) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
self.emit('buildData', build, data);
|
|
|
|
|
|
|
|
var cleanupText = function(text) {
|
|
|
|
return text.replace('\r', '');
|
|
|
|
};
|
|
|
|
|
|
|
|
var splittedData = data.split('\n'),
|
|
|
|
logLineNumber = self.buildLogLineNumbersHash[build.id] || 0;
|
|
|
|
|
|
|
|
self.lastLinesHash[build.id] = self.lastLinesHash[build.id] || '';
|
|
|
|
|
|
|
|
// if we don't have last line, so we start new line
|
|
|
|
if (!self.lastLinesHash[build.id]) {
|
|
|
|
logLineNumber++;
|
|
|
|
}
|
|
|
|
self.lastLinesHash[build.id] += _(splittedData).first();
|
|
|
|
|
|
|
|
var lines = [{
|
|
|
|
text: cleanupText(self.lastLinesHash[build.id]),
|
|
|
|
buildId: build.id,
|
|
|
|
number: logLineNumber
|
|
|
|
}];
|
|
|
|
|
|
|
|
if (splittedData.length > 1) {
|
|
|
|
// if we have last '' we have to take all except last
|
|
|
|
// this shown that string ends with eol
|
|
|
|
if (_(splittedData).last() === '') {
|
|
|
|
self.lastLinesHash[build.id] = '';
|
|
|
|
splittedData = _(splittedData.slice(1)).initial();
|
|
|
|
} else {
|
|
|
|
self.lastLinesHash[build.id] = _(splittedData).last();
|
|
|
|
splittedData = _(splittedData).tail();
|
|
|
|
}
|
|
|
|
|
|
|
|
lines = lines.concat(_(splittedData).map(function(line) {
|
|
|
|
return {
|
|
|
|
text: cleanupText(line),
|
|
|
|
buildId: build.id,
|
|
|
|
number: ++logLineNumber
|
|
|
|
};
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
self.buildLogLineNumbersHash[build.id] = logLineNumber;
|
|
|
|
|
|
|
|
self.emit('buildLogLines', build, lines);
|
|
|
|
};
|
|
|
|
|
2015-06-14 23:27:58 +00:00
|
|
|
Distributor.prototype._updateWaitReasons = function() {
|
|
|
|
var self = this;
|
|
|
|
_(self.queue).each(function(item) {
|
|
|
|
var waitReasons = _(self.nodes).map(function(node) {
|
|
|
|
return node.getExecutorWaitReason(item.project);
|
|
|
|
});
|
|
|
|
|
|
|
|
var waitReason = _(waitReasons).compact().join(', ');
|
2015-06-15 18:40:01 +00:00
|
|
|
// set only non-empty reasons
|
|
|
|
if (waitReason && waitReason !== item.build.waitReason) {
|
2015-06-14 23:27:58 +00:00
|
|
|
self._updateBuild(item.build, {waitReason: waitReason});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-06-15 18:40:01 +00:00
|
|
|
Distributor.prototype._onBuildComplete = function(build, callback) {
|
2015-06-13 18:59:32 +00:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
Steppy(
|
|
|
|
function() {
|
2015-06-14 21:15:26 +00:00
|
|
|
// notify about build
|
2016-01-10 16:37:19 +00:00
|
|
|
self.notifier.send(build);
|
2015-06-14 21:15:26 +00:00
|
|
|
|
2015-06-13 18:59:32 +00:00
|
|
|
// process after build triggers
|
|
|
|
var triggerAfterGroup = this.makeGroup();
|
|
|
|
|
|
|
|
var after = build.project.trigger && build.project.trigger.after;
|
|
|
|
if (after) {
|
|
|
|
_(after).each(function(item) {
|
|
|
|
if (!item.status || item.status === build.status) {
|
2015-06-13 19:08:13 +00:00
|
|
|
self.run({
|
2015-06-14 14:21:04 +00:00
|
|
|
projectName: item.project,
|
|
|
|
initiator: {
|
|
|
|
type: 'build',
|
|
|
|
id: build.id,
|
|
|
|
number: build.number,
|
|
|
|
project: {name: build.project.name}
|
2015-12-25 20:57:59 +00:00
|
|
|
}
|
2015-06-13 19:08:13 +00:00
|
|
|
}, triggerAfterGroup.slot());
|
2015-06-13 18:59:32 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
function(err, triggerAfterGroupResults) {
|
|
|
|
// try to run next project from the queue
|
|
|
|
self._runNext(this.slot());
|
|
|
|
},
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
Distributor.prototype._updateBuild = function(build, changes, callback) {
|
|
|
|
var self = this;
|
2015-05-05 23:11:28 +00:00
|
|
|
callback = callback || _.noop;
|
2015-12-15 17:29:13 +00:00
|
|
|
var isWithId = Boolean(build.id),
|
|
|
|
isWithNumber = Boolean(build.number);
|
2015-05-10 10:04:54 +00:00
|
|
|
|
|
|
|
Steppy(
|
|
|
|
function() {
|
2015-12-15 17:29:13 +00:00
|
|
|
if (build.id && changes.status && changes.status !== build.status) {
|
|
|
|
logger.log(
|
|
|
|
'Build #%s (project "%s") change status: %s -> %s',
|
|
|
|
build.id,
|
|
|
|
build.project.name,
|
|
|
|
build.status,
|
|
|
|
changes.status
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
_(build).extend(changes);
|
|
|
|
|
|
|
|
// skip saving to db of unimportant data
|
|
|
|
if (changes.currentStep && _(changes).keys().length === 1) {
|
|
|
|
this.pass(null);
|
|
|
|
} else {
|
|
|
|
self.saveBuild(build, this.slot());
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function() {
|
2015-12-15 17:29:13 +00:00
|
|
|
if (!isWithId && build.id) {
|
|
|
|
logger.log(
|
|
|
|
'Build #%s (project "%s") %s',
|
|
|
|
build.id,
|
|
|
|
build.project.name,
|
|
|
|
build.status
|
|
|
|
);
|
|
|
|
}
|
2015-05-17 10:26:28 +00:00
|
|
|
|
|
|
|
// if number appear after save to db then add it to changes
|
|
|
|
// TODO: might be better to generate number right there (instead
|
|
|
|
// of hooks)
|
|
|
|
if (!isWithNumber && build.number) {
|
|
|
|
changes.number = build.number;
|
|
|
|
}
|
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
// emits only after get an id (at save build)
|
|
|
|
self.emit('buildUpdate', build, changes);
|
|
|
|
|
|
|
|
this.pass(build);
|
|
|
|
},
|
|
|
|
callback
|
|
|
|
);
|
2015-11-24 22:41:20 +00:00
|
|
|
};
|
|
|
|
|
2016-01-07 20:48:31 +00:00
|
|
|
Distributor.prototype.cancel = function(id, callback) {
|
|
|
|
callback = callback || function(err) {
|
|
|
|
if (err) logger.error('Error during cancel: ', err.stack || err);
|
|
|
|
};
|
2015-11-24 22:41:20 +00:00
|
|
|
var self = this;
|
2016-01-07 20:48:31 +00:00
|
|
|
|
2015-11-24 22:41:20 +00:00
|
|
|
Steppy(
|
|
|
|
function() {
|
|
|
|
var queueItemIndex = _(self.queue).findIndex(function(item) {
|
2016-01-07 20:48:31 +00:00
|
|
|
return item.build.id === id;
|
2015-11-24 22:41:20 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (queueItemIndex === -1) {
|
|
|
|
throw new Error(
|
2016-01-07 20:48:31 +00:00
|
|
|
'Build with id "' + id + '" not found for cancel'
|
2015-11-24 22:41:20 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
);
|
2015-04-09 18:55:29 +00:00
|
|
|
};
|
|
|
|
|
2015-06-13 19:08:13 +00:00
|
|
|
Distributor.prototype.run = function(params, callback) {
|
2015-07-05 18:03:58 +00:00
|
|
|
callback = callback || function(err) {
|
2016-01-07 20:48:31 +00:00
|
|
|
if (err) logger.error('Error during run: ', err.stack || err);
|
2015-07-05 18:03:58 +00:00
|
|
|
};
|
2016-01-07 20:48:31 +00:00
|
|
|
var self = this,
|
|
|
|
project;
|
|
|
|
|
2015-04-09 18:55:29 +00:00
|
|
|
Steppy(
|
|
|
|
function() {
|
2016-01-05 11:18:20 +00:00
|
|
|
project = _(self.projects.get(params.projectName)).clone();
|
2015-07-11 10:41:01 +00:00
|
|
|
|
|
|
|
if (params.withScmChangesOnly) {
|
|
|
|
self.nodes[0].hasScmChanges(project, this.slot());
|
|
|
|
} else {
|
|
|
|
this.pass(null);
|
|
|
|
}
|
|
|
|
},
|
2015-07-28 20:40:26 +00:00
|
|
|
function(err, hasScmChanges) {
|
2015-07-11 10:41:01 +00:00
|
|
|
if (params.withScmChangesOnly && !hasScmChanges) {
|
|
|
|
logger.log(
|
|
|
|
'Building of "%s" skipped coz no scm changes',
|
|
|
|
project.name
|
|
|
|
);
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
|
2015-12-25 21:11:50 +00:00
|
|
|
if (!params.queueQueued) {
|
2015-12-10 22:26:18 +00:00
|
|
|
var queuedItem = _(self.queue).find(function(item) {
|
|
|
|
return item.project.name === project.name;
|
|
|
|
});
|
|
|
|
if (queuedItem) {
|
|
|
|
logger.log(
|
|
|
|
'Building of "%s" skipped coz it`s already queued',
|
|
|
|
project.name
|
|
|
|
);
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-10 10:04:54 +00:00
|
|
|
self._updateBuild({}, {
|
2015-04-09 18:55:29 +00:00
|
|
|
project: project,
|
2015-06-14 14:21:04 +00:00
|
|
|
initiator: params.initiator,
|
2015-06-13 19:08:13 +00:00
|
|
|
params: params.params,
|
2015-05-09 19:53:19 +00:00
|
|
|
createDate: Date.now(),
|
2015-05-21 19:04:38 +00:00
|
|
|
status: 'queued',
|
|
|
|
completed: false
|
2015-04-09 18:55:29 +00:00
|
|
|
}, this.slot());
|
|
|
|
},
|
|
|
|
function(err, build) {
|
|
|
|
self.queue.push({project: project, build: build});
|
|
|
|
self._runNext(this.slot());
|
|
|
|
},
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
};
|