From 34473131c6f3951cccd010247f03b0ccf0db687b Mon Sep 17 00:00:00 2001 From: Joscha Feth Date: Tue, 30 Apr 2019 14:42:35 +1000 Subject: [PATCH] fix: do not terminate random sessions This fixes the seemingly random `[chimp][hooks] Error: The test with session id XXX has already finished, and can't receive further commands` that happen when using chimp & saucelabs with a high concurrency. The current implementation of the saucelabs-manager retrieves 10 sessions of the current user and kills the first one that has been received, independent of whether that job is managed by the current build or not, which leads to job terminations on concurrently running tests in CI by the same user. --- src/lib/saucelabs-manager.js | 133 ++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 43 deletions(-) diff --git a/src/lib/saucelabs-manager.js b/src/lib/saucelabs-manager.js index 06f327b..627aa20 100644 --- a/src/lib/saucelabs-manager.js +++ b/src/lib/saucelabs-manager.js @@ -1,5 +1,6 @@ var request = require('request'), - log = require('./log'); + log = require('./log'), + _ = require('underscore'); /** * SessionManager Constructor @@ -21,6 +22,8 @@ function SauceLabsSessionManager(options) { this.retryDelay = 3000; this.retry = 0; + this.build = null; + log.debug('[chimp][saucelabs-session-manager] created a new SessionManager', options); } @@ -39,17 +42,25 @@ SauceLabsSessionManager.prototype.remote = function (webdriverOptions, callback) log.debug('[chimp][saucelabs-session-manager] creating webdriver remote '); var browser = this.webdriver.remote(webdriverOptions); + this.build = webdriverOptions.desiredCapabilities.build; + log.debug('[chimp][saucelabs-session-manager] build: ' + this.build); + callback(null, browser); return; }; +var DEFAULT_LIMIT = 100; + /** * Gets a list of sessions from the SauceLabs API based on Build ID * * @api private */ -SauceLabsSessionManager.prototype._getJobs = function (callback) { - var hub = this.options.sauceLabsUrl + '/jobs?full=:get_full_info&limit=10'; //default is 100 which seems like too much +SauceLabsSessionManager.prototype._getJobs = function (callback, limit, skip) { + var hub = this.options.sauceLabsUrl + '/jobs?full=true&limit=' + limit; + if (skip > 0) { + hub += '&skip=' + skip; + } log.debug('[chimp][saucelabs-session-manager]', 'requesting sessions from', hub); @@ -65,53 +76,89 @@ SauceLabsSessionManager.prototype._getJobs = function (callback) { }; /** - * Kills the 1st session found running on SauceLabs - * * @api public */ SauceLabsSessionManager.prototype.killCurrentSession = function (callback) { - this._getJobs(function (err, jobs) { - if (jobs && jobs.length) { - var job = jobs[0]; - - // This will stop the session, causing a 'User terminated' error. - // If we don't manually stop the session, we get a timed-out error. - var options = { - url: this.options.sauceLabsUrl + '/jobs/' + job.id + '/stop', - method: 'PUT' - }; - - request(options, function (error, response) { - if (!error && response.statusCode === 200) { - log.debug('[chimp][saucelabs-session-manager]', 'stopped session'); - callback(); - } else { - log.error('[chimp][saucelabs-session-manager]', 'received error', error); - callback(error); + var self = this; + var killSession = function (job) { + // This will stop the session, causing a 'User terminated' error. + // If we don't manually stop the session, we get a timed-out error. + var options = { + url: this.options.sauceLabsUrl + '/jobs/' + job.id + '/stop', + method: 'PUT' + }; + + request(options, function (error, response) { + if (!error && response.statusCode === 200) { + log.debug('[chimp][saucelabs-session-manager]', 'stopped session'); + callback(); + } else { + log.error('[chimp][saucelabs-session-manager]', 'received error', error); + callback(error); + } + }); + + // This will set the session to passing or else it will show as Errored out + // even though we stop it. + options = { + url: this.options.sauceLabsUrl + '/jobs/' + job.id, + method: 'PUT', + json: true, + body: { passed: true } + }; + + request(options, function (error, response) { + if (!error && response.statusCode === 200) { + log.debug('[chimp][saucelabs-session-manager]', 'updated session to passing state'); + callback(); + } else { + log.error('[chimp][saucelabs-session-manager]', 'received error', error); + callback(error); + } + }); + }.bind(this) + + var getJobForBuild = function(cb, skip) { + if (!self.build) { + // get one and kill it, this is the (flawed) default of chimp which will just randomly + // terminate sessions and you get an odd `Error: The test with session id XXX has already finished, and can't receive further commands.` error + console.warn('You should really consider setting a build, otherwise random sessions will be terminated!'); + this._getJobs(function(err, jobs) { + if (jobs.length) { + killSession(jobs[0]); } + }, 1); + return; + } + this._getJobs(function(err, jobs) { + // the original code never uses this error, + // probably because if we don't find anything we just leave the session alone + // we might want to revisit this behavior + // if (err) { + // cb(err); + // } + if (!jobs.length) { + // no more jobs found, let's exit + console.warn(`Couldn't find a job to terminate for build ${self.build}`); + callback(); + return; + } + var currentJob = _.find(jobs, function (b) { + return b.build === self.build; }); + if (!currentJob) { + // maybe it's in the next batch? + getJobForBuild(cb, skip + DEFAULT_LIMIT) + } else { + killSession(currentJob); + } + }, DEFAULT_LIMIT, skip); + }.bind(this) - // This will set the session to passing or else it will show as Errored out - // even though we stop it. - options = { - url: this.options.sauceLabsUrl + '/jobs/' + job.id, - method: 'PUT', - json: true, - body: { passed: true } - }; - - request(options, function (error, response) { - if (!error && response.statusCode === 200) { - log.debug('[chimp][saucelabs-session-manager]', 'updated session to passing state'); - callback(); - } else { - log.error('[chimp][saucelabs-session-manager]', 'received error', error); - callback(error); - } - }); - } - }.bind(this)); + + + getJobForBuild(killSession, 0); }; module.exports = SauceLabsSessionManager;