Escape builder names in url* and pathFor* methods of BuildbotSyncer
[WebKit-https.git] / Websites / perf.webkit.org / tools / js / buildbot-triggerable.js
1 'use strict';
2
3 let assert = require('assert');
4
5 require('./v3-models.js');
6
7 let BuildbotSyncer = require('./buildbot-syncer').BuildbotSyncer;
8
9 class BuildbotTriggerable {
10     constructor(config, remote, buildbotRemote, slaveInfo, logger)
11     {
12         this._name = config.triggerableName;
13         assert(typeof(this._name) == 'string', 'triggerableName must be specified');
14
15         this._lookbackCount = config.lookbackCount;
16         assert(typeof(this._lookbackCount) == 'number' && this._lookbackCount > 0, 'lookbackCount must be a number greater than 0');
17
18         this._remote = remote;
19
20         this._slaveInfo = slaveInfo;
21         assert(typeof(slaveInfo.name) == 'string', 'slave name must be specified');
22         assert(typeof(slaveInfo.password) == 'string', 'slave password must be specified');
23
24         this._syncers = BuildbotSyncer._loadConfig(buildbotRemote, config);
25         this._logger = logger || {log: function () { }, error: function () { }};
26     }
27
28     name() { return this._name; }
29
30     syncOnce()
31     {
32         let syncerList = this._syncers;
33         let buildReqeustsByGroup = new Map;
34
35         let self = this;
36         this._logger.log(`Fetching build requests for ${this._name}...`);
37         return BuildRequest.fetchForTriggerable(this._name).then(function () {
38             let buildRequests = BuildRequest.all();
39             self._validateRequests(buildRequests);
40             buildReqeustsByGroup = BuildbotTriggerable._testGroupMapForBuildRequests(buildRequests);
41             return self._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
42         }).then(function (updates) {
43             self._logger.log('Scheduling builds');
44             let promistList = [];
45             let testGroupList = Array.from(buildReqeustsByGroup.values()).sort(function (a, b) { return a.id - b.id; });
46             for (let group of testGroupList) {
47                 let promise = self._scheduleNextRequestInGroupIfSlaveIsAvailable(group, updates);
48                 if (promise)
49                     promistList.push(promise);
50             }
51             return Promise.all(promistList);
52         }).then(function () {
53             // Pull all buildbots for the second time since the previous step may have scheduled more builds.
54             return self._pullBuildbotOnAllSyncers(buildReqeustsByGroup);
55         }).then(function (updates) {
56             // FIXME: Add a new API that just updates the requests.
57             return self._remote.postJSON(`/api/build-requests/${self._name}`, {
58                 'slaveName': self._slaveInfo.name,
59                 'slavePassword': self._slaveInfo.password,
60                 'buildRequestUpdates': updates});
61         }).then(function (response) {
62             if (response['status'] != 'OK')
63                 self._logger.log('Failed to update the build requests status: ' + response['status']);
64         })
65     }
66
67     _validateRequests(buildRequests)
68     {
69         let testPlatformPairs = {};
70         for (let request of buildRequests) {
71             if (!this._syncers.some(function (syncer) { return syncer.matchesConfiguration(request); })) {
72                 let key = request.platform().id + '-' + request.test().id();
73                 if (!(key in testPlatformPairs))
74                     this._logger.error(`No matching configuration for "${request.test().fullName()}" on "${request.platform().name()}".`);                
75                 testPlatformPairs[key] = true;
76             }
77         }
78     }
79
80     _pullBuildbotOnAllSyncers(buildReqeustsByGroup)
81     {
82         let updates = {};
83         let associatedRequests = new Set;
84         let self = this;
85         return Promise.all(this._syncers.map(function (syncer) {
86             return syncer.pullBuildbot(self._lookbackCount).then(function (entryList) {
87                 for (let entry of entryList) {
88                     let request = BuildRequest.findById(entry.buildRequestId());
89                     if (!request)
90                         continue;
91                     associatedRequests.add(request);
92
93                     let info = buildReqeustsByGroup.get(request.testGroupId());
94                     assert(!info.syncer || info.syncer == syncer);
95                     info.syncer = syncer;
96                     if (entry.slaveName()) {
97                         assert(!info.slaveName || info.slaveName == entry.slaveName());
98                         info.slaveName = entry.slaveName();
99                     }
100
101                     let newStatus = entry.buildRequestStatusIfUpdateIsNeeded(request);
102                     if (newStatus) {
103                         self._logger.log(`Updating the status of build request ${request.id()} from ${request.status()} to ${newStatus}`);
104                         updates[entry.buildRequestId()] = {status: newStatus, url: entry.url()};
105                     } else if (!request.statusUrl()) {
106                         self._logger.log(`Setting the status URL of build request ${request.id()} to ${entry.url()}`);
107                         updates[entry.buildRequestId()] = {status: request.status(), url: entry.url()};
108                     }
109                 }
110             });
111         })).then(function () {
112             for (let request of BuildRequest.all()) {
113                 if (request.hasStarted() && !request.hasFinished() && !associatedRequests.has(request)) {
114                     self._logger.log(`Updating the status of build request ${request.id()} from ${request.status()} to failedIfNotCompleted`);
115                     assert(!(request.id() in updates));
116                     updates[request.id()] = {status: 'failedIfNotCompleted'};
117                 }
118             }
119         }).then(function () { return updates; });
120     }
121
122     _scheduleNextRequestInGroupIfSlaveIsAvailable(groupInfo, pendingUpdates)
123     {
124         let orderedRequests = groupInfo.requests.sort(function (a, b) { return a.order() - b.order(); });
125         let nextRequest = null;
126         for (let request of orderedRequests) {
127             if (request.isScheduled() || (request.id() in pendingUpdates && pendingUpdates[request.id()]['status'] == 'scheduled'))
128                 break;
129             if (request.isPending() && !(request.id() in pendingUpdates)) {
130                 nextRequest = request;
131                 break;
132             }
133         }
134         if (!nextRequest)
135             return null;
136
137         let firstRequest = !nextRequest.order();
138         if (firstRequest) {
139             this._logger.log(`Scheduling build request ${nextRequest.id()} on ${groupInfo.slaveName} in ${groupInfo.syncer.builderName()}`);
140             return groupInfo.syncer.scheduleRequest(request, groupInfo.slaveName);
141         }
142
143         for (let syncer of this._syncers) {
144             let promise = syncer.scheduleFirstRequestInGroupIfAvailable(nextRequest);
145             if (promise) {
146                 let slaveName = groupInfo.slaveName ? ` on ${groupInfo.slaveName}` : '';
147                 this._logger.log(`Scheduling build request ${nextRequest.id()}${slaveName} in ${syncer.builderName()}`);
148                 return promise;
149             }
150         }
151         return null;
152     }
153
154     static _testGroupMapForBuildRequests(buildRequests)
155     {
156         let map = new Map;
157         for (let request of buildRequests) {
158             let groupId = request.testGroupId();
159             if (!map.has(groupId)) // Don't use real TestGroup objects to avoid executing postgres query in the server
160                 map.set(groupId, {id: groupId, requests: [request], syncer: null, slaveName: null});
161             else
162                 map.get(groupId).requests.push(request);
163         }
164         return map;
165     }
166 }
167
168 if (typeof module != 'undefined')
169     module.exports.BuildbotTriggerable = BuildbotTriggerable;