mirror of
https://github.com/bigchaindb/js-bigchaindb-driver.git
synced 2024-12-28 15:47:50 +01:00
handle timeout for each request
This commit is contained in:
parent
b30578d9ab
commit
11892a1f6b
@ -40,9 +40,34 @@ const fetch = fetchPonyfill(Promise)
|
|||||||
* @return {Promise} Promise that will resolve with the response if its status was 2xx;
|
* @return {Promise} Promise that will resolve with the response if its status was 2xx;
|
||||||
* otherwise rejects with the response
|
* otherwise rejects with the response
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const timeout = (ms, promise) => new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const errorObject = {
|
||||||
|
message: 'TimeoutError'
|
||||||
|
}
|
||||||
|
reject(new Error(errorObject))
|
||||||
|
}, ms)
|
||||||
|
return promise.then(resolve, reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleResponse = (res) => {
|
||||||
|
// If status is not a 2xx (based on Response.ok), assume it's an error
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
|
||||||
|
if (!(res && res.ok)) {
|
||||||
|
const errorObject = {
|
||||||
|
message: 'HTTP Error: Requested page not reachable',
|
||||||
|
status: `${res.status} ${res.statusText}`,
|
||||||
|
requestURI: res.url
|
||||||
|
}
|
||||||
|
throw errorObject
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export default function baseRequest(url, {
|
export default function baseRequest(url, {
|
||||||
jsonBody, query, urlTemplateSpec, ...fetchConfig
|
jsonBody, query, urlTemplateSpec, ...fetchConfig
|
||||||
} = {}) {
|
} = {}, requestTimeout) {
|
||||||
let expandedUrl = url
|
let expandedUrl = url
|
||||||
|
|
||||||
if (urlTemplateSpec != null) {
|
if (urlTemplateSpec != null) {
|
||||||
@ -73,19 +98,11 @@ export default function baseRequest(url, {
|
|||||||
if (jsonBody != null) {
|
if (jsonBody != null) {
|
||||||
fetchConfig.body = JSON.stringify(jsonBody)
|
fetchConfig.body = JSON.stringify(jsonBody)
|
||||||
}
|
}
|
||||||
|
if (requestTimeout) {
|
||||||
|
return timeout(requestTimeout, fetch.fetch(expandedUrl, fetchConfig))
|
||||||
|
.then(handleResponse)
|
||||||
|
} else {
|
||||||
return fetch.fetch(expandedUrl, fetchConfig)
|
return fetch.fetch(expandedUrl, fetchConfig)
|
||||||
.then((res) => {
|
.then(handleResponse)
|
||||||
// If status is not a 2xx (based on Response.ok), assume it's an error
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch/fetch
|
|
||||||
if (!(res && res.ok)) {
|
|
||||||
const errorObject = {
|
|
||||||
message: 'HTTP Error: Requested page not reachable',
|
|
||||||
status: `${res.status} ${res.statusText}`,
|
|
||||||
requestURI: res.url
|
|
||||||
}
|
}
|
||||||
throw errorObject
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import Transport from './transport'
|
|||||||
|
|
||||||
const HEADER_BLACKLIST = ['content-type']
|
const HEADER_BLACKLIST = ['content-type']
|
||||||
const DEFAULT_NODE = 'http://localhost:9984/api/v1/'
|
const DEFAULT_NODE = 'http://localhost:9984/api/v1/'
|
||||||
|
const DEFAULT_TIMEOUT = 20000 // The default value is 20 seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -19,7 +20,7 @@ const DEFAULT_NODE = 'http://localhost:9984/api/v1/'
|
|||||||
|
|
||||||
export default class Connection {
|
export default class Connection {
|
||||||
// 20 seconds is the default value for a timeout if not specified
|
// 20 seconds is the default value for a timeout if not specified
|
||||||
constructor(nodes, headers = {}, timeout = 20000) {
|
constructor(nodes, headers = {}, timeout = DEFAULT_TIMEOUT) {
|
||||||
// Copy object
|
// Copy object
|
||||||
this.headers = Object.assign({}, headers)
|
this.headers = Object.assign({}, headers)
|
||||||
|
|
||||||
@ -48,7 +49,6 @@ export default class Connection {
|
|||||||
if (typeof node === 'string') {
|
if (typeof node === 'string') {
|
||||||
return { 'endpoint': node, 'headers': headers }
|
return { 'endpoint': node, 'headers': headers }
|
||||||
} else {
|
} else {
|
||||||
// TODO normalize URL if needed
|
|
||||||
const allHeaders = Object.assign({}, headers, node.headers)
|
const allHeaders = Object.assign({}, headers, node.headers)
|
||||||
return { 'endpoint': node.endpoint, 'headers': allHeaders }
|
return { 'endpoint': node.endpoint, 'headers': allHeaders }
|
||||||
}
|
}
|
||||||
@ -70,8 +70,8 @@ export default class Connection {
|
|||||||
}[endpoint]
|
}[endpoint]
|
||||||
}
|
}
|
||||||
|
|
||||||
_req(pathEndpoint, options = {}) {
|
_req(path, options = {}) {
|
||||||
return this.transport.forwardRequest(pathEndpoint, options)
|
return this.transport.forwardRequest(path, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,8 +11,8 @@ const DEFAULT_REQUEST_CONFIG = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BACKOFF_DELAY = 0.5 // seconds
|
const BACKOFF_DELAY = 500 // 0.5 seconds
|
||||||
|
const ERROR = 'HTTP Error: Requested page not reachable'
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* Small wrapper around js-utility-belt's request that provides url resolving,
|
* Small wrapper around js-utility-belt's request that provides url resolving,
|
||||||
@ -28,30 +28,31 @@ export default class Request {
|
|||||||
this.connectionError = null
|
this.connectionError = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async request(endpoint, config, timeout, maxBackoffTime) {
|
async request(urlPath, config, timeout, maxBackoffTime) {
|
||||||
|
if (!urlPath) {
|
||||||
|
return Promise.reject(new Error('Request was not given a url.'))
|
||||||
|
}
|
||||||
// Load default fetch configuration and remove any falsy query parameters
|
// Load default fetch configuration and remove any falsy query parameters
|
||||||
const requestConfig = Object.assign({}, this.node.headers, DEFAULT_REQUEST_CONFIG, config, {
|
const requestConfig = Object.assign({}, this.node.headers, DEFAULT_REQUEST_CONFIG, config, {
|
||||||
query: config.query && sanitize(config.query)
|
query: config.query && sanitize(config.query)
|
||||||
})
|
})
|
||||||
const apiUrl = this.node.endpoint + endpoint
|
const apiUrl = this.node.endpoint + urlPath
|
||||||
if (requestConfig.jsonBody) {
|
if (requestConfig.jsonBody) {
|
||||||
requestConfig.headers = Object.assign({}, requestConfig.headers, {
|
requestConfig.headers = Object.assign({}, requestConfig.headers, {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!endpoint) {
|
// If connectionError occurs, a timestamp equal to now +
|
||||||
return Promise.reject(new Error('Request was not given a url.'))
|
// `backoffTimedelta` is assigned to the object.
|
||||||
}
|
|
||||||
|
|
||||||
// If `ConnectionError` occurs, a timestamp equal to now +
|
|
||||||
// the default delay (`BACKOFF_DELAY`) is assigned to the object.
|
|
||||||
// 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`.
|
// waits till the timestamp is passed or raises `TimeoutError`.
|
||||||
// If `ConnectionError` occurs two or more times in a row,
|
// If `ConnectionError` occurs two or more times in a row,
|
||||||
// the retry count is incremented and the new timestamp is calculated
|
// the retry count is incremented and the new timestamp is calculated
|
||||||
// as now + the default delay multiplied by two to the power of the
|
// as now + the `backoffTimedelta`
|
||||||
// number of retries.
|
// The `backoffTimedelta` is the minimum between the default delay
|
||||||
|
// multiplied by two to the power of the
|
||||||
|
// number of retries or timeout/2 or 10. See Transport class for that
|
||||||
// If a request is successful, the backoff timestamp is removed,
|
// If a request is successful, the backoff timestamp is removed,
|
||||||
// the retry count is back to zero.
|
// the retry count is back to zero.
|
||||||
|
|
||||||
@ -66,9 +67,13 @@ export default class Request {
|
|||||||
if (backoffTimedelta > 0) {
|
if (backoffTimedelta > 0) {
|
||||||
await Request.sleep(backoffTimedelta)
|
await Request.sleep(backoffTimedelta)
|
||||||
}
|
}
|
||||||
// this.timeout = setTimeout ? setTimeout - backoffTimedelta : setTimeout
|
|
||||||
return baseRequest(apiUrl, requestConfig)
|
const requestTimeout = timeout ? timeout - backoffTimedelta : timeout
|
||||||
.then(res => res.json())
|
return baseRequest(apiUrl, requestConfig, requestTimeout)
|
||||||
|
.then(async (res) => {
|
||||||
|
this.connectionError = null
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
// ConnectionError
|
// ConnectionError
|
||||||
this.connectionError = err
|
this.connectionError = err
|
||||||
@ -78,6 +83,26 @@ export default class Request {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateBackoffTime(maxBackoffTime) {
|
||||||
|
if (!this.connectionError) {
|
||||||
|
this.retries = 0
|
||||||
|
this.backoffTime = null
|
||||||
|
} else if (this.connectionError.message === ERROR) {
|
||||||
|
// If status is not a 2xx (based on Response.ok), throw error
|
||||||
|
this.retries = 0
|
||||||
|
this.backoffTime = null
|
||||||
|
throw this.connectionError
|
||||||
|
} else {
|
||||||
|
// Timeout or no connection could be stablished
|
||||||
|
const backoffTimedelta = Math.min(BACKOFF_DELAY * (2 ** this.retries), maxBackoffTime)
|
||||||
|
this.backoffTime = Date.now() + backoffTimedelta
|
||||||
|
this.retries += 1
|
||||||
|
if (this.connectionError.message === 'TimeoutError') {
|
||||||
|
throw this.connectionError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getBackoffTimedelta() {
|
getBackoffTimedelta() {
|
||||||
if (!this.backoffTime) {
|
if (!this.backoffTime) {
|
||||||
return 0
|
return 0
|
||||||
@ -85,17 +110,6 @@ export default class Request {
|
|||||||
return (this.backoffTime - Date.now())
|
return (this.backoffTime - Date.now())
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBackoffTime(maxBackoffTime) {
|
|
||||||
if (!this.connectionError) {
|
|
||||||
this.retries = 0
|
|
||||||
this.backoffTime = null
|
|
||||||
} else {
|
|
||||||
const backoffTimedelta = Math.min(BACKOFF_DELAY * (2 ** this.retries), maxBackoffTime)
|
|
||||||
this.backoffTime = Date.now() + backoffTimedelta
|
|
||||||
this.retries += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static sleep(ms) {
|
static sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export default class Transport {
|
|||||||
this.connectionPool = []
|
this.connectionPool = []
|
||||||
this.timeout = timeout
|
this.timeout = timeout
|
||||||
// the maximum backoff time is 10 seconds
|
// the maximum backoff time is 10 seconds
|
||||||
this.maxBackoffTime = timeout ? timeout / 10 : 10000
|
this.maxBackoffTime = timeout ? timeout / 2 : 10000
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
this.connectionPool.push(new Request(node))
|
this.connectionPool.push(new Request(node))
|
||||||
})
|
})
|
||||||
@ -28,9 +28,6 @@ export default class Transport {
|
|||||||
// Select the connection with the earliest backoff time, in case of a tie,
|
// Select the connection with the earliest backoff time, in case of a tie,
|
||||||
// prefer the one with the smaller list index
|
// prefer the one with the smaller list index
|
||||||
pickConnection() {
|
pickConnection() {
|
||||||
if (this.connectionPool.length === 1) {
|
|
||||||
return this.connectionPool[0]
|
|
||||||
}
|
|
||||||
let connection = this.connectionPool[0]
|
let connection = this.connectionPool[0]
|
||||||
|
|
||||||
this.connectionPool.forEach(conn => {
|
this.connectionPool.forEach(conn => {
|
||||||
@ -58,7 +55,7 @@ export default class Transport {
|
|||||||
this.maxBackoffTime
|
this.maxBackoffTime
|
||||||
)
|
)
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
if (connection.backoffTime) {
|
if (connection.backoffTime && this.timeout) {
|
||||||
this.timeout -= elapsed
|
this.timeout -= elapsed
|
||||||
} else {
|
} else {
|
||||||
// No connection error, the response is valid
|
// No connection error, the response is valid
|
||||||
@ -71,6 +68,6 @@ export default class Transport {
|
|||||||
const errorObject = {
|
const errorObject = {
|
||||||
message: 'TimeoutError',
|
message: 'TimeoutError',
|
||||||
}
|
}
|
||||||
throw connection.connectionError ? connection.connectionError : errorObject
|
throw errorObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ const conn = new Connection(API_PATH)
|
|||||||
|
|
||||||
test('Payload thrown at incorrect API_PATH', async t => {
|
test('Payload thrown at incorrect API_PATH', async t => {
|
||||||
const path = 'http://localhost:9984/api/wrong/'
|
const path = 'http://localhost:9984/api/wrong/'
|
||||||
const connection = new Connection(path, {}, 0)
|
const connection = new Connection(path)
|
||||||
const target = {
|
const target = {
|
||||||
message: 'HTTP Error: Requested page not reachable',
|
message: 'HTTP Error: Requested page not reachable',
|
||||||
status: '404 NOT FOUND',
|
status: '404 NOT FOUND',
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
|
|
||||||
test('Pick connection with earliest backoff time', async t => {
|
test('Pick connection with earliest backoff time', async t => {
|
||||||
const path1 = 'http://localhost:9984/api/v1/'
|
const path1 = 'http://localhost:9984/api/v1/'
|
||||||
const path2 = 'http://localhost:9984/api/wrong/'
|
const path2 = 'http://localhostwrong:9984/api/v1/'
|
||||||
|
|
||||||
// Reverse order
|
// Reverse order
|
||||||
const conn = new Connection([path2, path1])
|
const conn = new Connection([path2, path1])
|
||||||
|
Loading…
Reference in New Issue
Block a user