diff --git a/package.json b/package.json index 193c3e2..fbac4b0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "dev": "webpack -w", "clean": "rimraf dist/bundle dist/node", "test": "npm run lint && nyc ava test/ && npm run thanks && npm run report-coverage", - "mine-test": "nyc ava test/connection/test_mine*", "thanks": "cowsay Hi, thanks for your interest in BigchainDB. We appreciate your contribution!", "release": "./node_modules/release-it/bin/release-it.js --src.tagName='v%s' --github.release --npm.publish --non-interactive", "release-minor": "./node_modules/release-it/bin/release-it.js minor --src.tagName='v%s' --github.release --npm.publish --non-interactive", diff --git a/src/connection.js b/src/connection.js index 41223f4..293ca2e 100644 --- a/src/connection.js +++ b/src/connection.js @@ -1,4 +1,4 @@ -import Transport from './Transport' +import Transport from './transport' const HEADER_BLACKLIST = ['content-type'] const DEFAULT_NODE = 'http://localhost:9984' @@ -44,7 +44,7 @@ export default class Connection { } else { // TODO normalize URL if needed const allHeaders = Object.assign({}, headers, node.headers) - return { 'endpoint': node, 'headers': allHeaders } + return { 'endpoint': node.endpoint, 'headers': allHeaders } } } @@ -125,7 +125,6 @@ export default class Connection { * @param operation */ listTransactions(assetId, operation) { - console.log('listtransaction', assetId) return this._req(Connection.getApiUrls('transactions'), { query: { asset_id: assetId, diff --git a/src/index.js b/src/index.js index 24953b7..bbc98ee 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,6 @@ export Ed25519Keypair from './Ed25519Keypair' export Connection from './connection' -export Request from './request' export Transaction from './transaction' export ccJsonLoad from './utils/ccJsonLoad' export ccJsonify from './utils/ccJsonify' diff --git a/src/request.js b/src/request.js index 258579f..13d4e76 100644 --- a/src/request.js +++ b/src/request.js @@ -21,11 +21,11 @@ export default class Request { this.node = node this.requestConfig = requestConfig this.backoffTime = null + this.retries = 0 + this.connectionError = null } - async request(endpoint, config, timeout) { - // Num or retries to the same node - this.retries = 0 + async request(endpoint, config, setTimeout) { // Load default fetch configuration and remove any falsy query parameters const requestConfig = Object.assign({}, this.node.headers, DEFAULT_REQUEST_CONFIG, config, { query: config.query && sanitize(config.query) @@ -43,7 +43,7 @@ export default class Request { // If `ConnectionError` occurs, a timestamp equal to now + // the default delay (`BACKOFF_DELAY`) is assigned to the object. - // The timestamp is in UTC. Next time the function is called, it either + // Next time the function is called, it either // waits till the timestamp is passed or raises `TimeoutError`. // If `ConnectionError` occurs two or more times in a row, // the retry count is incremented and the new timestamp is calculated @@ -52,28 +52,27 @@ export default class Request { // If a request is successful, the backoff timestamp is removed, // the retry count is back to zero. - this.backoffTimedelta = this.getBackoffTimedelta() + const backoffTimedelta = this.getBackoffTimedelta() - if (timeout != null && timeout < this.backoffTimedelta) { - throw new Error() + if (setTimeout != null && setTimeout < this.backoffTimedelta) { + const errorObject = { + message: 'TimeoutError' + } + throw errorObject } - if (this.backoffTimedelta > 0) { - await Request.sleep(this.backoffTimedelta) + if (backoffTimedelta > 0) { + await Request.sleep(backoffTimedelta) } - this.timeout = this.timeout ? this.timeout - this.backoffTimedelta : timeout - + this.timeout = setTimeout ? setTimeout - backoffTimedelta : setTimeout return baseRequest(apiUrl, requestConfig) - .then(res => async function handleResponse() { - res.json() - if (!(res.status >= 200 && res.status < 300)) { - console.log('Valid response') - } - }) + .then(res => res.json()) .catch(err => { - throw err + // ConnectionError + this.connectionError = err + // throw err }) - .finally((res) => { - this.updateBackoffTime(res) + .finally(() => { + this.updateBackoffTime() }) } @@ -84,13 +83,13 @@ export default class Request { return (this.backoffTime - Date.now()) } - updateBackoffTime(success) { - if (success) { + updateBackoffTime() { + if (!this.connectionError) { this.retries = 0 this.backoffTime = null } else { - this.backoffTimedelta = BACKOFF_DELAY * (2 ** this.retries) - this.backoffTime = Date.now() + this.backoffTimedelta + const backoffTimedelta = BACKOFF_DELAY * (2 ** this.retries) + this.backoffTime = Date.now() + backoffTimedelta this.retries += 1 } } diff --git a/src/Transport.js b/src/transport.js similarity index 64% rename from src/Transport.js rename to src/transport.js index 375f328..5ddaf90 100644 --- a/src/Transport.js +++ b/src/transport.js @@ -1,3 +1,7 @@ +// Copyright BigchainDB GmbH and BigchainDB contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + import Request from './request' @@ -15,11 +19,8 @@ export default class Transport { if (this.connectionPool.length === 1) { return this.connectionPool[0] } - return this.minBackoff() - } - - minBackoff() { let connection = this.connectionPool[0] + this.connectionPool.forEach(conn => { // 0 the lowest value is the time for Thu Jan 01 1970 01:00:00 GMT+0100 (CET) conn.backoffTime = conn.backoffTime ? conn.backoffTime : 0 @@ -29,28 +30,36 @@ export default class Transport { } async forwardRequest(path, headers) { + let response + let connection while (!this.timeout || this.timeout > 0) { - const connection = this.pickConnection() - + connection = this.pickConnection() // Date in milliseconds const startTime = Date.now() try { - // TODO wait until request is done - const response = connection.request( + // eslint-disable-next-line no-await-in-loop + response = await connection.request( path, headers, this.timeout ) - return response + const elapsed = Date.now() - startTime + if (connection.backoffTime) { + this.timeout += elapsed + } else { + return response + } + + if (connection.retries > 3) { + throw connection.connectionError + } } catch (err) { throw err - } finally { - const elapsed = Date.now() - startTime - if (this.timeout) { - this.timeout -= elapsed - } } } - throw new Error() + const errorObject = { + message: 'Timeout error', + } + throw errorObject } } diff --git a/test/connection/test_connection.js b/test/connection/test_connection.js index 573cc2b..e1a6c60 100644 --- a/test/connection/test_connection.js +++ b/test/connection/test_connection.js @@ -1,8 +1,12 @@ import test from 'ava' import sinon from 'sinon' -import { Connection, Request } from '../../src' -import { API_PATH } from '../constants' +import { + Connection +} from '../../src' +import { + API_PATH +} from '../constants' const conn = new Connection(API_PATH) @@ -37,22 +41,59 @@ test('Generate API URLS', t => { }) }) -// TODO Redefine test -test('Request with custom headers', t => { - const testConn = new Connection(API_PATH, { hello: 'world' }) - const expectedOptions = { +test('Normalize node from an object', t => { + const headers = { + custom: 'headers' + } + const node = { + endpoint: API_PATH, headers: { + hello: 'world' + } + } + const expectedNode = { + 'endpoint': API_PATH, + 'headers': { hello: 'world', custom: 'headers' } } - // request is read only, cannot be mocked? - sinon.spy(Request, 'default') - testConn._req(API_PATH, { headers: { custom: 'headers' } }) + t.deepEqual(Connection.normalizeNode(node, headers), expectedNode) +}) - t.truthy(Request.default.calledWith(API_PATH, expectedOptions)) - Request.default.restore() +test('Normalize node from a string', t => { + const headers = { + custom: 'headers' + } + const expectedNode = { + 'endpoint': API_PATH, + 'headers': { + custom: 'headers' + } + } + + t.deepEqual(Connection.normalizeNode(API_PATH, headers), expectedNode) +}) + +test('Request with custom headers', t => { + const testConn = new Connection(API_PATH, { + hello: 'world' + }) + const expectedOptions = { + headers: { + custom: 'headers' + } + } + const PATH = 'blocks' + testConn.transport.forwardRequest = sinon.spy() + + testConn._req(PATH, { + headers: { + custom: 'headers' + } + }) + t.truthy(testConn.transport.forwardRequest.calledWith(PATH, expectedOptions)) }) diff --git a/test/integration/test_integration.js b/test/integration/test_integration.js index 9862822..0daee19 100644 --- a/test/integration/test_integration.js +++ b/test/integration/test_integration.js @@ -36,7 +36,9 @@ test('Valid CREATE transaction', t => { const txSigned = Transaction.signTransaction(tx, alice.privateKey) return conn.postTransaction(txSigned) - .then(resTx => t.truthy(resTx)) + .then(resTx => { + t.truthy(resTx) + }) }) diff --git a/test/transport/test_transport.js b/test/transport/test_transport.js new file mode 100644 index 0000000..5c70ad8 --- /dev/null +++ b/test/transport/test_transport.js @@ -0,0 +1,35 @@ +// Copyright BigchainDB GmbH and BigchainDB contributors +// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +// Code is Apache-2.0 and docs are CC-BY-4.0 + +import test from 'ava' + +import { + Connection +} from '../../src' + + +test('Pick connection with earliest backoff time', t => { + const path1 = 'http://localhost:9984/api/v1/' + const path2 = 'http://localhost:9984/api/wrong/' + + const conn = new Connection([path1, path2]) + + conn.searchAssets('example') + const connection1 = conn.transport.connectionPool[0] + + t.deepEqual(conn.transport.pickConnection(), connection1) +}) + +test('Pick connection with earliest backoff time', async t => { + const path1 = 'http://localhost:9984/api/v1/' + const path2 = 'http://localhost:9984/api/wrong/' + + // Reverse order + const conn = new Connection([path2, path1]) + await conn.searchAssets('example') + + const connection1 = conn.transport.connectionPool[1] + + t.deepEqual(conn.transport.pickConnection(), connection1) +})