1 | "use strict";
|
2 |
|
3 | const _ = require('lodash');
|
4 | const Promise = require('bluebird');
|
5 | const {GridError, ArgError} = require('../errors');
|
6 | const {gridMessages} = require('../commons/constants');
|
7 | const logger = require('../commons/logger').getLogger("grid-service");
|
8 | const servicesApi = require('../commons/testimServicesApi');
|
9 |
|
10 | const gridCache = {};
|
11 | const urlExtractRegex = /(^(https?):\/{2})?(.*)/;
|
12 | let keepAliveTimer = null;
|
13 |
|
14 | function extractProtocol(hostUrl) {
|
15 | const urlExtract = urlExtractRegex.exec(hostUrl);
|
16 |
|
17 | return urlExtract[2] || 'https';
|
18 | }
|
19 |
|
20 | function extractHost(hostUrl) {
|
21 | const urlExtract = urlExtractRegex.exec(hostUrl);
|
22 |
|
23 | return urlExtract[3];
|
24 | }
|
25 |
|
26 | function 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 |
|
42 | function 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 |
|
76 | function addItemToGridCache(workerId, gridId, slotId, browser) {
|
77 | gridCache[workerId] = {gridId, slotId, browser};
|
78 | }
|
79 |
|
80 | function getHostAndPortById(workerId, projectId, gridId, browser, executionId) {
|
81 | return handleGetGridResponse(projectId, workerId, browser, () => servicesApi.getGridById(projectId, gridId, browser, executionId));
|
82 | }
|
83 |
|
84 | function getHostAndPortByName(workerId, projectId, gridName, browser, executionId) {
|
85 | return handleGetGridResponse(projectId, workerId, browser, () => servicesApi.getGridByProject(projectId, gridName, browser, executionId));
|
86 | }
|
87 |
|
88 | function getAllGrids(companyId) {
|
89 | return servicesApi.getAllGrids(companyId);
|
90 | }
|
91 |
|
92 | function 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 |
|
103 | function 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 |
|
114 | function 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 |
|
132 | function 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 |
|
147 | function startKeepAlive(projectId) {
|
148 | const KEEP_ALIVE_INTERVAL = 10 * 1000;
|
149 | keepAliveTimer = setInterval(keepAlive, KEEP_ALIVE_INTERVAL, projectId);
|
150 | }
|
151 |
|
152 | function 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 |
|
164 | function endKeepAlive(projectId) {
|
165 | return releaseAllSlots(projectId)
|
166 | .then(() => clearInterval(keepAliveTimer));
|
167 | }
|
168 |
|
169 | function 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 |
|
179 | function getVendorUserFromOptions(type, options) {
|
180 | const {saucelabs} = options;
|
181 | if (type === "saucelabs") {
|
182 | return saucelabs.username;
|
183 | }
|
184 | }
|
185 |
|
186 | function 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 |
|
209 | function 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 |
|
229 | const getGridSlot = Promise.method(_getGridSlot);
|
230 |
|
231 | async 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 |
|
256 | module.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 | };
|