UNPKG

8.85 kBJavaScriptView Raw
1"use strict";
2
3const _ = require('lodash');
4const Promise = require('bluebird');
5const {GridError, ArgError} = require('../errors');
6const {gridMessages} = require('../commons/constants');
7const logger = require('../commons/logger').getLogger("grid-service");
8const servicesApi = require('../commons/testimServicesApi');
9
10const gridCache = {};
11const urlExtractRegex = /(^(https?):\/{2})?(.*)/;
12let keepAliveTimer = null;
13
14function extractProtocol(hostUrl) {
15 const urlExtract = urlExtractRegex.exec(hostUrl);
16
17 return urlExtract[2] || 'https';
18}
19
20function extractHost(hostUrl) {
21 const urlExtract = urlExtractRegex.exec(hostUrl);
22
23 return urlExtract[3];
24}
25
26function getSerializableObject(grid) {
27 const host = grid && extractHost(grid.host);
28 const port = grid && grid.port;
29 const protocol = grid && grid.type === 'testimEnterprise' ? extractProtocol(grid.host) : undefined;
30 const accessToken = grid && grid.token;
31 const slotId = grid && grid.slotId;
32 const user = grid && grid.external && grid.external.user;
33 const key = grid && grid.external && grid.external.key;
34 const arn = grid && grid.external && grid.external.arn;
35 const type = grid && grid.type;
36 const name = grid && grid.name;
37 const gridId = grid && grid._id;
38
39 return { host, port, protocol, accessToken, slotId, gridId, user, key, type, name, arn };
40}
41
42function handleGetGridResponse(projectId, workerId, browser, getFun) {
43 return getFun()
44 .catch(err => {
45 logger.error("failed to get grid", {projectId, err});
46 throw new Error(gridMessages.UNKNOWN);
47 })
48 .then(res => {
49 logger.info("get grid info", Object.assign({}, res, {projectId}));
50 const isSuccess = () => res.status === "success";
51 const isError = () => res.status === "error" && res.code;
52 if (!res || (!isError() && !isSuccess())) {
53 logger.error("invalid response - get grid", {res});
54 throw new Error(gridMessages.UNKNOWN);
55 }
56
57 if (isSuccess()) {
58 const serGrid = getSerializableObject(res.grid);
59 addItemToGridCache(workerId, serGrid.gridId, serGrid.slotId, browser);
60 return serGrid;
61 }
62
63 if (isError() && res.code === "not-found") {
64 throw new GridError(gridMessages.NOT_FOUND);
65 }
66
67 if (isError() && res.code === "no-available-slot") {
68 throw new GridError(`Failed to run test on ${browser} - concurrency limit reached`);
69 }
70
71 logger.error("invalid code error response - get grid", {res});
72 throw new GridError(gridMessages.UNKNOWN);
73 });
74}
75
76function addItemToGridCache(workerId, gridId, slotId, browser) {
77 gridCache[workerId] = {gridId, slotId, browser};
78}
79
80function getHostAndPortById(workerId, projectId, gridId, browser, executionId) {
81 return handleGetGridResponse(projectId, workerId, browser, () => servicesApi.getGridById(projectId, gridId, browser, executionId));
82}
83
84function getHostAndPortByName(workerId, projectId, gridName, browser, executionId) {
85 return handleGetGridResponse(projectId, workerId, browser, () => servicesApi.getGridByProject(projectId, gridName, browser, executionId));
86}
87
88function getAllGrids(companyId) {
89 return servicesApi.getAllGrids(companyId);
90}
91
92function getGridDataByGridId(companyId, gridId, allGrids) {
93 return Promise.resolve(allGrids || getAllGrids(companyId))
94 .then(grids => {
95 const grid = grids.find(grid => grid._id === gridId);
96 if (!grid) {
97 throw new ArgError(`Failed to find grid id: ${gridId}`);
98 }
99 return getSerializableObject(grid);
100 });
101}
102
103function getGridDataByGridName(companyId, gridName, allGrids) {
104 return Promise.resolve(allGrids || getAllGrids(companyId))
105 .then(grids => {
106 const grid = grids.find(grid => (grid.name || "").toLowerCase() === gridName.toLowerCase());
107 if (!grid) {
108 throw new ArgError(`Failed to find grid name: ${gridName}`);
109 }
110 return getSerializableObject(grid);
111 });
112}
113
114function releaseGridSlot(workerId, projectId) {
115 const gridData = gridCache[workerId];
116 if (!gridData) {
117 return Promise.resolve();
118 }
119
120 const {slotId, gridId, browser} = gridData;
121 delete gridCache[workerId];
122 if (!slotId) {
123 logger.warn("failed to find grid slot id", {projectId});
124 return Promise.resolve();
125 }
126
127 logger.info("release slot id", {projectId, slotId, gridId, browser});
128 return servicesApi.releaseGridSlot(projectId, slotId, gridId, browser)
129 .catch(err => logger.error("failed to release slot", {projectId, err}));
130}
131
132function keepAlive(projectId) {
133 const slots = Object.keys(gridCache).reduce((slots, workerId) => {
134 slots.push(gridCache[workerId]);
135 return slots;
136 }, []).filter(Boolean);
137
138 if (_.isEmpty(slots)) {
139 return Promise.resolve();
140 }
141
142 logger.info("keep alive worker slots", {projectId, slots});
143 return servicesApi.keepAliveGrid(projectId, slots)
144 .catch(err => logger.error("failed to update grid keep alive", {err, slots, projectId}));
145}
146
147function startKeepAlive(projectId) {
148 const KEEP_ALIVE_INTERVAL = 10 * 1000;
149 keepAliveTimer = setInterval(keepAlive, KEEP_ALIVE_INTERVAL, projectId);
150}
151
152function releaseAllSlots(projectId) {
153 const workerIds = Object.keys(gridCache);
154
155 if (_.isEmpty(workerIds)) {
156 return Promise.resolve();
157 }
158
159 logger.warn("not all slots released before end runner flow", {projectId});
160 return Promise.map(workerIds, workerId => releaseGridSlot(workerId, projectId))
161 .catch(err => logger.error("failed to release all slots", {err, projectId}));
162}
163
164function endKeepAlive(projectId) {
165 return releaseAllSlots(projectId)
166 .then(() => clearInterval(keepAliveTimer));
167}
168
169function getVendorKeyFromOptions(type, options) {
170 const {testobjectSauce, saucelabs} = options;
171 if (type === "testobject") {
172 return testobjectSauce.testobjectApiKey;
173 }
174 if (type === "saucelabs") {
175 return saucelabs.accessKey;
176 }
177}
178
179function getVendorUserFromOptions(type, options) {
180 const {saucelabs} = options;
181 if (type === "saucelabs") {
182 return saucelabs.username;
183 }
184}
185
186function getOptionGrid(options) {
187 const getGridType = () => {
188 if (!_.isEmpty(options.testobjectSauce)) {
189 return "testobject";
190 }
191
192 if (!_.isEmpty(options.saucelabs)) {
193 return "saucelabs";
194 }
195
196 if (!_.isEmpty(options.perfecto)) {
197 return "perfecto";
198 }
199
200 return "hostAndPort";
201 };
202 const type = getGridType();
203 const {host, port, path, protocol} = options;
204 const key = getVendorKeyFromOptions(type, options);
205 const user = getVendorUserFromOptions(type, options);
206 return Promise.resolve({host, port, path, protocol, type, user, key});
207}
208
209function getGridData(options, allGrids = undefined) {
210 const {host, grid, gridId, company, useLocalChromeDriver} = options;
211 const companyId = company.companyId;
212
213 if (host) {
214 return Promise.resolve(getOptionGrid(options));
215 }
216 if (useLocalChromeDriver === true) {
217 return Promise.resolve({ mode: 'local' });
218 }
219 if (gridId) {
220 return getGridDataByGridId(companyId, gridId, allGrids);
221 }
222 if (grid) {
223 return getGridDataByGridName(companyId, grid, allGrids);
224 }
225
226 return Promise.reject(new GridError("Missing host or grid configuration"));
227}
228
229const getGridSlot = Promise.method(_getGridSlot);
230
231async function _getGridSlot(browser, executionId, testResultId, onGridSlot, options, workerId) {
232 const getGridDataFromServer = () => {
233 const {host, project, grid, gridId, useLocalChromeDriver} = options;
234 if (host) {
235 return Promise.resolve(getOptionGrid(options));
236 }
237 if (gridId) {
238 return getHostAndPortById(workerId, project, gridId, browser, executionId);
239 }
240 if (grid) {
241 return getHostAndPortByName(workerId, project, grid, browser, executionId);
242 }
243 if (useLocalChromeDriver) {
244 return { mode: 'local' };
245 }
246 throw new GridError("Missing host or grid configuration");
247 };
248
249 const gridInfo = await getGridDataFromServer();
250
251 await onGridSlot(executionId, testResultId, gridInfo);
252
253 return gridInfo;
254}
255
256module.exports = {
257 getGridSlot: getGridSlot,
258 releaseGridSlot: releaseGridSlot,
259 getGridData: getGridData,
260 addItemToGridCache: addItemToGridCache,
261 getSerializableObject: getSerializableObject,
262 keepAlive: {
263 start: startKeepAlive,
264 end: endKeepAlive
265 }
266};