blob: 9eed10655a1de7d3aac0910a3ed937da4c35cc92 [file] [log] [blame]
'use strict';
let assert = require('assert');
require('./v3-models.js');
let BuildbotSyncer = require('./buildbot-syncer').BuildbotSyncer;
class BuildbotTriggerable {
constructor(config, remote, buildbotRemote, slaveInfo, logger)
{
this._name = config.triggerableName;
assert(typeof(this._name) == 'string', 'triggerableName must be specified');
this._lookbackCount = config.lookbackCount;
assert(typeof(this._lookbackCount) == 'number' && this._lookbackCount > 0, 'lookbackCount must be a number greater than 0');
this._remote = remote;
this._config = config;
this._buildbotRemote = buildbotRemote;
this._slaveInfo = slaveInfo;
assert(typeof(slaveInfo.name) == 'string', 'slave name must be specified');
assert(typeof(slaveInfo.password) == 'string', 'slave password must be specified');
this._syncers = null;
this._logger = logger || {log: () => { }, error: () => { }};
}
getBuilderNameToIDMap()
{
return this._buildbotRemote.getJSON("/api/v2/builders").then((content) => {
assert(content.builders instanceof Array);
const builderNameToIDMap = {};
for (const builder of content.builders)
builderNameToIDMap[builder.name] = builder.builderid;
return builderNameToIDMap;
});
}
initSyncers()
{
return this.getBuilderNameToIDMap().then((builderNameToIDMap) => {
this._syncers = BuildbotSyncer._loadConfig(this._buildbotRemote, this._config, builderNameToIDMap);
});
}
name() { return this._name; }
updateTriggerable()
{
const map = new Map;
let repositoryGroups = [];
for (const syncer of this._syncers) {
for (const config of syncer.testConfigurations()) {
const entry = {test: config.test.id(), platform: config.platform.id()};
map.set(entry.test + '-' + entry.platform, entry);
}
// FIXME: Move BuildbotSyncer._loadConfig here and store repository groups directly.
repositoryGroups = syncer.repositoryGroups();
}
return this._remote.postJSONWithStatus(`/api/update-triggerable/`, {
'slaveName': this._slaveInfo.name,
'slavePassword': this._slaveInfo.password,
'triggerable': this._name,
'configurations': Array.from(map.values()),
'repositoryGroups': Object.keys(repositoryGroups).map((groupName) => {
const group = repositoryGroups[groupName];
return {
name: groupName,
description: group.description,
acceptsRoots: group.acceptsRoots,
repositories: group.repositoryList,
};
})});
}
syncOnce()
{
let syncerList = this._syncers;
let buildReqeustsByGroup = new Map;
this._logger.log(`Fetching build requests for ${this._name}...`);
let validRequests;
return BuildRequest.fetchForTriggerable(this._name).then((buildRequests) => {
validRequests = this._validateRequests(buildRequests);
buildReqeustsByGroup = BuildbotTriggerable._testGroupMapForBuildRequests(buildRequests);
return this._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
}).then((updates) => {
this._logger.log('Scheduling builds');
const promistList = [];
const testGroupList = Array.from(buildReqeustsByGroup.values()).sort(function (a, b) { return a.groupOrder - b.groupOrder; });
for (const group of testGroupList) {
const nextRequest = this._nextRequestInGroup(group, updates);
if (!validRequests.has(nextRequest))
continue;
const promise = this._scheduleRequestIfSlaveIsAvailable(nextRequest, group.requests,
nextRequest.isBuild() ? group.buildSyncer : group.testSyncer,
nextRequest.isBuild() ? group.buildSlaveName : group.testSlaveName);
if (promise)
promistList.push(promise);
}
return Promise.all(promistList);
}).then(() => {
// Pull all buildbots for the second time since the previous step may have scheduled more builds.
return this._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
}).then((updates) => {
// FIXME: Add a new API that just updates the requests.
return this._remote.postJSONWithStatus(`/api/build-requests/${this._name}`, {
'slaveName': this._slaveInfo.name,
'slavePassword': this._slaveInfo.password,
'buildRequestUpdates': updates});
});
}
_validateRequests(buildRequests)
{
const testPlatformPairs = {};
const validatedRequests = new Set;
for (let request of buildRequests) {
if (!this._syncers.some((syncer) => syncer.matchesConfiguration(request))) {
const key = request.platform().id + '-' + (request.isBuild() ? 'build' : request.test().id());
const kind = request.isBuild() ? 'Building' : `"${request.test().fullName()}"`;
if (!(key in testPlatformPairs))
this._logger.error(`Build request ${request.id()} has no matching configuration: ${kind} on "${request.platform().name()}".`);
testPlatformPairs[key] = true;
continue;
}
const triggerable = request.triggerable();
if (!triggerable) {
this._logger.error(`Build request ${request.id()} does not specify a valid triggerable`);
continue;
}
assert(triggerable instanceof Triggerable, 'Must specify a valid triggerable');
assert.equal(triggerable.name(), this._name, 'Must specify the triggerable of this syncer');
const repositoryGroup = request.repositoryGroup();
if (!repositoryGroup) {
this._logger.error(`Build request ${request.id()} does not specify a repository group. Such a build request is no longer supported.`);
continue;
}
const acceptedGroups = triggerable.repositoryGroups();
if (!acceptedGroups.includes(repositoryGroup)) {
const acceptedNames = acceptedGroups.map((group) => group.name()).join(', ');
this._logger.error(`Build request ${request.id()} specifies ${repositoryGroup.name()} but triggerable ${this._name} only accepts ${acceptedNames}`);
continue;
}
validatedRequests.add(request);
}
return validatedRequests;
}
_pullBuildbotOnAllSyncers(buildReqeustsByGroup)
{
let updates = {};
let associatedRequests = new Set;
return Promise.all(this._syncers.map((syncer) => {
return syncer.pullBuildbot(this._lookbackCount).then((entryList) => {
for (const entry of entryList) {
const request = BuildRequest.findById(entry.buildRequestId());
if (!request)
continue;
associatedRequests.add(request);
const info = buildReqeustsByGroup.get(request.testGroupId());
if (request.isBuild()) {
assert(!info.buildSyncer || info.buildSyncer == syncer);
if (entry.slaveName()) {
assert(!info.buildSlaveName || info.buildSlaveName == entry.slaveName());
info.buildSlaveName = entry.slaveName();
}
info.buildSyncer = syncer;
} else {
assert(!info.testSyncer || info.testSyncer == syncer);
if (entry.slaveName()) {
assert(!info.testSlaveName || info.testSlaveName == entry.slaveName());
info.testSlaveName = entry.slaveName();
}
info.testSyncer = syncer;
}
const newStatus = entry.buildRequestStatusIfUpdateIsNeeded(request);
if (newStatus) {
this._logger.log(`Updating the status of build request ${request.id()} from ${request.status()} to ${newStatus}`);
updates[entry.buildRequestId()] = {status: newStatus, url: entry.url(), statusDescription: entry.statusDescription()};
} else if (!request.statusUrl() || request.statusDescription() != entry.statusDescription()) {
this._logger.log(`Updating build request ${request.id()} status URL to ${entry.url()} and status detail from ${request.statusDescription()} to ${entry.statusDescription()}`);
updates[entry.buildRequestId()] = {status: request.status(), url: entry.url(), statusDescription: entry.statusDescription()};
}
}
});
})).then(() => {
for (const request of BuildRequest.all()) {
if (request.hasStarted() && !request.hasFinished() && !associatedRequests.has(request)) {
this._logger.log(`Updating the status of build request ${request.id()} from ${request.status()} to failedIfNotCompleted`);
assert(!(request.id() in updates));
updates[request.id()] = {status: 'failedIfNotCompleted'};
}
}
}).then(() => updates);
}
_nextRequestInGroup(groupInfo, pendingUpdates)
{
for (const request of groupInfo.requests) {
if (request.isScheduled() || (request.id() in pendingUpdates && pendingUpdates[request.id()]['status'] == 'scheduled'))
return null;
if (request.isPending() && !(request.id() in pendingUpdates))
return request;
if (request.isBuild() && !request.hasCompleted())
return null; // A build request is still pending, scheduled, running, or failed.
}
return null;
}
_scheduleRequestIfSlaveIsAvailable(nextRequest, requestsInGroup, syncer, slaveName)
{
if (!nextRequest)
return null;
const isFirstRequest = nextRequest == requestsInGroup[0] || !nextRequest.order();
if (!isFirstRequest) {
if (syncer)
return this._scheduleRequestWithLog(syncer, nextRequest, requestsInGroup, slaveName);
this._logger.error(`Could not identify the syncer for ${nextRequest.id()}.`);
}
// Pick a new syncer for the first test.
for (const syncer of this._syncers) {
const promise = this._scheduleRequestWithLog(syncer, nextRequest, requestsInGroup, null);
if (promise)
return promise;
}
return null;
}
_scheduleRequestWithLog(syncer, request, requestsInGroup, slaveName)
{
const promise = syncer.scheduleRequestInGroupIfAvailable(request, requestsInGroup, slaveName);
if (!promise)
return promise;
this._logger.log(`Scheduling build request ${request.id()}${slaveName ? ' on ' + slaveName : ''} in ${syncer.builderName()}`);
return promise;
}
static _testGroupMapForBuildRequests(buildRequests)
{
const map = new Map;
let groupOrder = 0;
for (let request of buildRequests) {
let groupId = request.testGroupId();
if (!map.has(groupId)) // Don't use real TestGroup objects to avoid executing postgres query in the server
map.set(groupId, {id: groupId, groupOrder: groupOrder++, requests: [request], buildSyncer: null, testSyncer: null, slaveName: null});
else
map.get(groupId).requests.push(request);
}
return map;
}
}
if (typeof module != 'undefined')
module.exports.BuildbotTriggerable = BuildbotTriggerable;