From 396bed8873c25c12888ae852d8bf7fbd1f6ac809 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Tue, 16 Feb 2016 17:09:53 -0300 Subject: [PATCH] add multiple insight server support --- config.js | 2 + lib/blockchainexplorers/insight.js | 49 +++---- lib/blockchainexplorers/request-pull.js | 57 ++++++++ package.json | 26 ++-- test/request-pull.js | 170 ++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 34 deletions(-) create mode 100644 lib/blockchainexplorers/request-pull.js create mode 100644 test/request-pull.js diff --git a/config.js b/config.js index 9afe4eb..c0479c3 100644 --- a/config.js +++ b/config.js @@ -43,6 +43,8 @@ var config = { testnet: { provider: 'insight', url: 'https://test-insight.bitpay.com:443', + // Multiple servers (in priority order) + // url: ['http://a.b.c', 'https://test-insight.bitpay.com:443'], }, }, pushNotificationsOpts: { diff --git a/lib/blockchainexplorers/insight.js b/lib/blockchainexplorers/insight.js index 950930f..f3d5c43 100644 --- a/lib/blockchainexplorers/insight.js +++ b/lib/blockchainexplorers/insight.js @@ -4,8 +4,8 @@ var _ = require('lodash'); var $ = require('preconditions').singleton(); var log = require('npmlog'); log.debug = log.verbose; -var request = require('request'); var io = require('socket.io-client'); +var prequest = require('./request-pull'); function Insight(opts) { $.checkArgument(opts); @@ -14,7 +14,7 @@ function Insight(opts) { this.apiPrefix = opts.apiPrefix || '/api'; this.network = opts.network || 'livenet'; - this.url = opts.url; + this.hosts = opts.url; }; @@ -28,23 +28,23 @@ var _parseErr = function(err, res) { }; Insight.prototype.getConnectionInfo = function() { - return 'Insight (' + this.network + ') @ ' + this.url; + return 'Insight (' + this.network + ') @ ' + this.hosts; }; /** * Retrieve a list of unspent outputs associated with an address or set of addresses */ Insight.prototype.getUnspentUtxos = function(addresses, cb) { - var url = this.url + this.apiPrefix + '/addrs/utxo'; var args = { method: 'POST', - url: url, + hosts: this.hosts, + path: this.apiPrefix + '/addrs/utxo', json: { addrs: [].concat(addresses).join(',') }, }; - request(args, function(err, res, unspent) { + prequest(args, function(err, res, unspent) { if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); return cb(null, unspent); }); @@ -54,30 +54,30 @@ Insight.prototype.getUnspentUtxos = function(addresses, cb) { * Broadcast a transaction to the bitcoin network */ Insight.prototype.broadcast = function(rawTx, cb) { - var url = this.url + this.apiPrefix + '/tx/send'; var args = { method: 'POST', - url: url, + hosts: this.hosts, + path: this.apiPrefix + '/tx/send', json: { rawtx: rawTx }, }; - request(args, function(err, res, body) { + prequest(args, function(err, res, body) { if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); return cb(null, body ? body.txid : null); }); }; Insight.prototype.getTransaction = function(txid, cb) { - var url = this.url + this.apiPrefix + '/tx/' + txid; var args = { method: 'GET', - url: url, + hosts: this.hosts, + path: this.apiPrefix + '/tx/' + txid, json: true, }; - request(args, function(err, res, tx) { + prequest(args, function(err, res, tx) { if (res && res.statusCode == 404) return cb(); if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); @@ -91,16 +91,16 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) { if (_.isNumber(from)) qs.push('from=' + from); if (_.isNumber(to)) qs.push('to=' + to); - var url = this.url + this.apiPrefix + '/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''); var args = { method: 'POST', - url: url, + hosts: this.hosts, + path: this.apiPrefix + '/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''), json: { addrs: [].concat(addresses).join(',') }, }; - request(args, function(err, res, txs) { + prequest(args, function(err, res, txs) { if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); if (_.isObject(txs) && txs.items) @@ -116,14 +116,14 @@ Insight.prototype.getTransactions = function(addresses, from, to, cb) { Insight.prototype.getAddressActivity = function(address, cb) { var self = this; - var url = self.url + self.apiPrefix + '/addr/' + address; var args = { method: 'GET', - url: url, + hosts: this.hosts, + path: self.apiPrefix + '/addr/' + address, json: true, }; - request(args, function(err, res, result) { + prequest(args, function(err, res, result) { if (res && res.statusCode == 404) return cb(); if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); @@ -134,24 +134,27 @@ Insight.prototype.getAddressActivity = function(address, cb) { }; Insight.prototype.estimateFee = function(nbBlocks, cb) { - var url = this.url + this.apiPrefix + '/utils/estimatefee'; + var path = this.apiPrefix + '/utils/estimatefee'; if (nbBlocks) { - url += '?nbBlocks=' + [].concat(nbBlocks).join(','); + path += '?nbBlocks=' + [].concat(nbBlocks).join(','); } var args = { method: 'GET', - url: url, + hosts: this.hosts, + path: path, json: true, }; - request(args, function(err, res, body) { + prequest(args, function(err, res, body) { if (err || res.statusCode !== 200) return cb(_parseErr(err, res)); return cb(null, body); }); }; Insight.prototype.initSocket = function() { - var socket = io.connect(this.url, { + + // sockets always use the first server on the pull + var socket = io.connect(this.hosts[0], { 'reconnection': true, }); return socket; diff --git a/lib/blockchainexplorers/request-pull.js b/lib/blockchainexplorers/request-pull.js new file mode 100644 index 0000000..944b455 --- /dev/null +++ b/lib/blockchainexplorers/request-pull.js @@ -0,0 +1,57 @@ +var _ = require('lodash'); +var async = require('async'); +var $ = require('preconditions').singleton(); + +var log = require('npmlog'); +log.debug = log.verbose; + +/** + * Query a server, using one of the given options + * + * @param {Object} opts + * @param {Array} opts.hosts Array of hosts to query. Until the first success one. + * @param {Array} opts.path Path to request in each server + */ +var requestPull = function(args, cb) { + $.checkArgument(args.hosts); + request = args.request || require('request'); + + if (!_.isArray(args.hosts)) + args.hosts = [args.hosts]; + + var urls = _.map(args.hosts, function(x) { + return (x + args.path); + }); + var nextUrl, result, success; + + async.whilst( + function() { + nextUrl = urls.shift(); + return nextUrl && !success; + }, + function(a_cb) { + args.uri = nextUrl; + request(args, function(err, res, body) { + if (err) { + log.warn('REQUEST FAIL: ' + nextUrl + ' ERROR: ' + err); + } + + if (res) { + success = !!res.statusCode.toString().match(/^[1234]../); + if (!success) { + log.warn('REQUEST FAIL: ' + nextUrl + ' STATUS CODE: ' + res.statusCode); + } + } + + result = [err, res, body]; + return a_cb(); + }); + }, + function(err) { + if (err) return cb(err); + return cb(result[0], result[1], result[2]); + } + ); +}; + +module.exports = requestPull; diff --git a/package.json b/package.json index 5fcad8a..fd20c7d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/bitpay/bitcore-wallet-service/issues" }, "dependencies": { - "async": "^0.9.0", + "async": "^0.9.2", "bitcore-lib": "^0.13.7", "body-parser": "^1.11.0", "coveralls": "^2.11.2", @@ -65,14 +65,18 @@ "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "bitcoreNode": "./bitcorenode", - "contributors": [{ - "name": "Braydon Fuller", - "email": "braydon@bitpay.com" - }, { - "name": "Ivan Socolsky", - "email": "ivan@bitpay.com" - }, { - "name": "Matias Alejo Garcia", - "email": "ematiu@gmail.com" - }] + "contributors": [ + { + "name": "Braydon Fuller", + "email": "braydon@bitpay.com" + }, + { + "name": "Ivan Socolsky", + "email": "ivan@bitpay.com" + }, + { + "name": "Matias Alejo Garcia", + "email": "ematiu@gmail.com" + } + ] } diff --git a/test/request-pull.js b/test/request-pull.js new file mode 100644 index 0000000..6163bd2 --- /dev/null +++ b/test/request-pull.js @@ -0,0 +1,170 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var prequest = require('../lib/blockchainexplorers/request-pull'); + +describe('request-pull', function() { + var request; + + beforeEach(function() { + request = sinon.stub(); + }); + it('should support url as string', function(done) { + + request.yields(null, { + statusCode: 200 + }, 'abc'); + + prequest({ + hosts: 'url1', + request: request, + }, function(err, res, body) { + should.not.exist(err); + body.should.be.equal('abc'); + res.statusCode.should.be.equal(200); + done(); + }); + }); + it('should support url as string (500 response)', function(done) { + request.yields(null, { + statusCode: 500 + }); + prequest({ + hosts: 'url1', + request: request, + }, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.be.equal(500); + done(); + }); + }); + it('should support url as array of strings', function(done) { + request.yields(null, { + statusCode: 200 + }, 'abc'); + prequest({ + hosts: ['url1', 'url2'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + body.should.be.equal('abc'); + done(); + }); + }); + it('should try 2nd url if first is unsuccessful (5xx)', function(done) { + request.onCall(0).yields(null, { + statusCode: 500 + }); + request.onCall(1).yields(null, { + statusCode: 550 + }); + prequest({ + hosts: ['url1', 'url2'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.be.equal(550); + done(); + }); + }); + it('should query 3th url if first 2 are unsuccessful (5xx)', function(done) { + request.onCall(0).yields(null, { + statusCode: 500 + }); + request.onCall(1).yields(null, { + statusCode: 550 + }); + request.onCall(2).yields(null, { + statusCode: 200, + }, 'abc'); + prequest({ + hosts: ['url1', 'url2', 'url3'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + body.should.be.equal('abc'); + done(); + }); + }); + it('should query only the first url if response is 404', function(done) { + request.onCall(0).yields(null, { + statusCode: 404 + }); + request.onCall(1).yields(null, { + statusCode: 550 + }); + prequest({ + hosts: ['url1', 'url2'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.be.equal(404); + done(); + }); + }); + it('should query only the first 2 urls if the second is successfull (5xx)', function(done) { + request.onCall(0).yields(null, { + statusCode: 500 + }); + request.onCall(1).yields(null, { + statusCode: 200, + }, '2nd'); + request.onCall(2).yields(null, { + statusCode: 200, + }, 'abc'); + prequest({ + hosts: ['url1', 'url2', 'url3'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + body.should.be.equal('2nd'); + res.statusCode.should.be.equal(200); + done(); + }); + }); + it('should query only the first 2 urls if the second is successfull (timeout)', function(done) { + request.onCall(0).yields({ + code: 'ETIMEDOUT', + connect: true + }); + request.onCall(1).yields(null, { + statusCode: 200, + }, '2nd'); + request.onCall(2).yields(null, { + statusCode: 200, + }, 'abc'); + prequest({ + hosts: ['url1', 'url2', 'url3'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + body.should.be.equal('2nd'); + res.statusCode.should.be.equal(200); + done(); + }); + + }); + it('should use the latest response if all requests are unsuccessfull', function(done) { + request.onCall(0).yields({ + code: 'ETIMEDOUT', + connect: true + }); + request.onCall(1).yields(null, { + statusCode: 505, + }, '2nd'); + request.onCall(2).yields(null, { + statusCode: 510, + }, 'abc'); + prequest({ + hosts: ['url1', 'url2', 'url3'], + request: request, + }, function(err, res, body) { + should.not.exist(err); + res.statusCode.should.be.equal(510); + done(); + }); + }); +});