Skip to content

Commit

Permalink
Handle download failures and configurable download retries
Browse files Browse the repository at this point in the history
Handle download failures better and allow to configure a retry similar
to the connect retry.
  • Loading branch information
johanneswuerbach committed Jan 8, 2017
1 parent 5dbf04d commit 5d5d3a4
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 24 deletions.
8 changes: 7 additions & 1 deletion README.md
Expand Up @@ -108,7 +108,13 @@ var sauceConnectLauncher = require('sauce-connect-launcher'),
connectRetries: 0

// time to wait between connection retries in ms. (optional)
connectRetryTimeout: 5000
connectRetryTimeout: 2000

// retry to download the sauce connect archive multiple times. (optional)
downloadRetries: 0

// time to wait between download retries in ms. (optional)
downloadRetryTimeout: 1000

// path to a sauce connect executable (optional)
// by default the latest sauce connect version is downloaded
Expand Down
13 changes: 12 additions & 1 deletion lib/process_options.js
Expand Up @@ -17,7 +17,18 @@ module.exports = function processOptions(options) {
options.accessKey = options.accessKey || process.env.SAUCE_ACCESS_KEY;

return _.reduce(
_.omit(options, ["readyFileId", "verbose", "logger", "log", "connectRetries", "connectRetryTimeout", "detached", "connectVersion"]),
_.omit(options, [
"readyFileId",
"verbose",
"logger",
"log",
"connectRetries",
"connectRetryTimeout",
"downloadRetries",
"downloadRetryTimeout",
"detached",
"connectVersion"
]),
function (argList, value, key) {
if (typeof value === "undefined" || value === null) {
return argList;
Expand Down
40 changes: 36 additions & 4 deletions lib/sauce-connect-launcher.js
Expand Up @@ -21,7 +21,9 @@ var
require("../package.json").sauceConnectLauncher.scVersion,
tunnelIdRegExp = /Tunnel ID:\s*([a-z0-9]+)/i,
portRegExp = /port\s*([0-9]+)/i,
tryRun = require("./try_run");
tryRun = require("./try_run"),
defaultConnectRetryTimeout = 2000,
defaultDownloadRetryTimeout = 1000;

function setWorkDir(workDir) {
scDir = workDir;
Expand Down Expand Up @@ -187,6 +189,10 @@ function fetchArchive(archiveName, archivefile, callback) {
path: "/downloads/" + archiveName
});

req.on("error", function (err) {
callback(err);
});

function removeArchive() {
try {
logger("Removing " + archivefile);
Expand All @@ -205,6 +211,12 @@ function fetchArchive(archiveName, archivefile, callback) {
logger("This will only happen once.");

req.on("response", function (res) {
if (res.statusCode !== 200) {
logger(`Invalid response status: ${res.statusCode}`);

return callback(new Error("Download failed with status code: " + res.statusCode));
}

var len = parseInt(res.headers["content-length"], 10),
prettyLen = (len / (1024 * 1024) + "").substr(0, 4);

Expand Down Expand Up @@ -314,8 +326,14 @@ function getVersion(options, cb) {
path: "/versions.json"
});

req.on("error", function (err) {
cb(err);
});

req.on("response", function (res) {
if (res.statusCode !== 200) {
logger(`Invalid response status: ${res.statusCode}`);

return cb(new Error("Fetching https://saucelabs.com/versions.json failed: " + res.statusCode));
}

Expand Down Expand Up @@ -565,11 +583,25 @@ function connect(bin, options, callback) {
}

function run(version, options, callback) {
tryRun(0, options, function (tryCallback) {
tryRun(0, {
logger: options.logger,
retries: options.connectRetries,
timeout: options.connectRetryTimeout || defaultConnectRetryTimeout
}, function (tryCallback) {
return connect(version, options, tryCallback);
}, callback);
}

function downloadWithRety(options, callback) {
tryRun(0, {
logger: options.logger,
retries: options.downloadRetries,
timeout: options.downloadRetryTimeout || defaultDownloadRetryTimeout
}, function (tryCallback) {
return download(options, tryCallback);
}, callback);
}

function downloadAndRun(options, callback) {
if (arguments.length === 1) {
callback = options;
Expand All @@ -578,15 +610,15 @@ function downloadAndRun(options, callback) {
logger = options.logger || function () {};

async.waterfall([
async.apply(download, options),
async.apply(downloadWithRety, options),
function (bin, next) {
return run(bin, options, next);
},
], callback);
}

module.exports = downloadAndRun;
module.exports.download = download;
module.exports.download = downloadWithRety;
module.exports.kill = killProcesses;
module.exports.clean = clean;
module.exports.setWorkDir = setWorkDir;
10 changes: 4 additions & 6 deletions lib/try_run.js
@@ -1,12 +1,10 @@
var defaultConnectRetryTimeout = 2000;

module.exports = function tryRun(count, options, fn, callback) {
module.exports = function tryRun(count, retryOptions, fn, callback) {
fn(function (err, result) {
if (err) {
if (count < options.connectRetries) {
if (count < retryOptions.retries) {
return setTimeout(function () {
tryRun(count + 1, options, fn, callback);
}, options.connectRetryTimeout || defaultConnectRetryTimeout);
tryRun(count + 1, retryOptions, fn, callback);
}, retryOptions.timeout);
}

return callback(err);
Expand Down
2 changes: 2 additions & 0 deletions test/process_options.test.js
Expand Up @@ -67,6 +67,8 @@ describe("processOptions", function () {
logger: function () {},
connectRetries: 1,
connectRetryTimeout: 5000,
downloadRetries: 1,
downloadRetryTimeout: 1000,
detached: true,
connectVersion: "1.2.3"
})).to.eql([]);
Expand Down
89 changes: 81 additions & 8 deletions test/sauce-connect-launcher.test.js
Expand Up @@ -24,6 +24,7 @@ try {
sauceCreds.log.push(message);
};
sauceCreds.connectRetries = 3;
sauceCreds.downloadRetries = 2;
} catch (e) {
require("colors");
console.log("Please run make setup-sauce to set up real Sauce Labs Credentials".red);
Expand Down Expand Up @@ -63,6 +64,7 @@ describe("Sauce Connect Launcher", function () {
it("fails with an invalid executable", function (done) {
var options = _.clone(sauceCreds);
options.exe = "not-found";
options.connectRetries = 0;

sauceConnectLauncher(options, function (err) {
expect(err).to.be.ok();
Expand All @@ -74,6 +76,7 @@ describe("Sauce Connect Launcher", function () {
it("does not trigger a download when providing a custom executable", function (done) {
var options = _.clone(sauceCreds);
options.exe = "not-found";
options.connectRetries = 0;

sauceConnectLauncher(options, function () {
expect(fs.existsSync(path.join(__dirname, "../sc/versions.json"))).not.to.be.ok();
Expand All @@ -84,14 +87,15 @@ describe("Sauce Connect Launcher", function () {
it("should download Sauce Connect", function (done) {
// We need to allow enough time for downloading Sauce Connect
var log = [];
sauceConnectLauncher.download({
logger: function (message) {
if (verbose) {
console.log("[info] ", message);
}
log.push(message);
},
}, function (err) {
var options = _.clone(sauceCreds);
options.logger = function (message) {
if (verbose) {
console.log("[info] ", message);
}
log.push(message);
};

sauceConnectLauncher.download(options, function (err) {
expect(err).to.not.be.ok();

// Expected command sequence
Expand All @@ -113,6 +117,75 @@ describe("Sauce Connect Launcher", function () {
});
});

it("handles errors when Sauce Connect download fails", function (done) {
var log = [];
var options = _.clone(sauceCreds);
options.logger = function (message) {
if (verbose) {
console.log("[info] ", message);
}
log.push(message);
};
options.connectVersion = "9.9.9";
options.downloadRetries = 1;

sauceConnectLauncher.download(options, function (err) {
expect(err).to.be.ok();
expect(err.message).to.contain("Download failed with status code");

// Expected command sequence
var expectedSequence = [
"Missing Sauce Connect local proxy, downloading dependency",
"This will only happen once.",
"Invalid response status: 404",
"Missing Sauce Connect local proxy, downloading dependency",
"This will only happen once."
];

_.each(log, function (message, i) {
expect(message).to.match(new RegExp("^" + (expectedSequence[i] || "\\*missing\\*")));
});

done();
});
});

describe("handles misconfigured proxies and other request failures", function () {
let options, http_proxy_original;

beforeEach(function () {
options = _.clone(sauceCreds);
options.downloadRetries = 0;

http_proxy_original = process.env.http_proxy;
process.env.http_proxy = "http://127.0.0.1:12345/";
})

afterEach(function () {
process.env.http_proxy = http_proxy_original;
})

it("when fetching versions.json", function (done) {
sauceConnectLauncher.download(options, function (err) {
expect(err).to.be.ok();
expect(err.message).to.contain("ECONNREFUSED");

done();
});
});

it("with fixed version when fetching archive", function (done) {
options.connectVersion = "9.9.9";

sauceConnectLauncher.download(options, function (err) {
expect(err).to.be.ok();
expect(err.message).to.contain("ECONNREFUSED");

done();
});
});
});

if (sauceCreds) {
it("should work with real credentials", function (done) {
sauceConnectLauncher(sauceCreds, function (err, sauceConnectProcess) {
Expand Down
7 changes: 3 additions & 4 deletions test/try_run_test.js
Expand Up @@ -19,10 +19,9 @@ describe("tryRun", function () {
});

describe("with configured retry", function () {
var retryTimeout = 10;
var retryOptions = {
connectRetries: 2,
connectRetryTimeout: retryTimeout
retries: 2,
timeout: 10
};

it("calls the provided function once when no error is returned", function (done) {
Expand All @@ -49,7 +48,7 @@ describe("tryRun", function () {
});
});

it("waits connectRetryTimeout between retries", function (done) {
it("waits timeout between retries", function (done) {
var start = Date.now();
tryRun(0, retryOptions, function (callback) {
callback(innerErr);
Expand Down

0 comments on commit 5d5d3a4

Please sign in to comment.