mirror of
https://github.com/bigchaindb/js-bigchaindb-driver.git
synced 2024-12-27 07:07: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;
|
||||
* 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, {
|
||||
jsonBody, query, urlTemplateSpec, ...fetchConfig
|
||||
} = {}) {
|
||||
} = {}, requestTimeout) {
|
||||
let expandedUrl = url
|
||||
|
||||
if (urlTemplateSpec != null) {
|
||||
@ -73,19 +98,11 @@ export default function baseRequest(url, {
|
||||
if (jsonBody != null) {
|
||||
fetchConfig.body = JSON.stringify(jsonBody)
|
||||
}
|
||||
|
||||
return fetch.fetch(expandedUrl, fetchConfig)
|
||||
.then((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
|
||||
})
|
||||
if (requestTimeout) {
|
||||
return timeout(requestTimeout, fetch.fetch(expandedUrl, fetchConfig))
|
||||
.then(handleResponse)
|
||||
} else {
|
||||
return fetch.fetch(expandedUrl, fetchConfig)
|
||||
.then(handleResponse)
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import Transport from './transport'
|
||||
|
||||
const HEADER_BLACKLIST = ['content-type']
|
||||
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 {
|
||||
// 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
|
||||
this.headers = Object.assign({}, headers)
|
||||
|
||||
@ -48,7 +49,6 @@ export default class Connection {
|
||||
if (typeof node === 'string') {
|
||||
return { 'endpoint': node, 'headers': headers }
|
||||
} else {
|
||||
// TODO normalize URL if needed
|
||||
const allHeaders = Object.assign({}, headers, node.headers)
|
||||
return { 'endpoint': node.endpoint, 'headers': allHeaders }
|
||||
}
|
||||
@ -70,8 +70,8 @@ export default class Connection {
|
||||
}[endpoint]
|
||||
}
|
||||
|
||||
_req(pathEndpoint, options = {}) {
|
||||
return this.transport.forwardRequest(pathEndpoint, options)
|
||||
_req(path, 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
|
||||
* Small wrapper around js-utility-belt's request that provides url resolving,
|
||||
@ -28,30 +28,31 @@ export default class Request {
|
||||
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
|
||||
const requestConfig = Object.assign({}, this.node.headers, DEFAULT_REQUEST_CONFIG, config, {
|
||||
query: config.query && sanitize(config.query)
|
||||
})
|
||||
const apiUrl = this.node.endpoint + endpoint
|
||||
const apiUrl = this.node.endpoint + urlPath
|
||||
if (requestConfig.jsonBody) {
|
||||
requestConfig.headers = Object.assign({}, requestConfig.headers, {
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
}
|
||||
|
||||
if (!endpoint) {
|
||||
return Promise.reject(new Error('Request was not given a url.'))
|
||||
}
|
||||
|
||||
// If `ConnectionError` occurs, a timestamp equal to now +
|
||||
// the default delay (`BACKOFF_DELAY`) is assigned to the object.
|
||||
// If connectionError occurs, a timestamp equal to now +
|
||||
// `backoffTimedelta` is assigned to the object.
|
||||
// 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
|
||||
// as now + the default delay multiplied by two to the power of the
|
||||
// number of retries.
|
||||
// as now + the `backoffTimedelta`
|
||||
// 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,
|
||||
// the retry count is back to zero.
|
||||
|
||||
@ -66,9 +67,13 @@ export default class Request {
|
||||
if (backoffTimedelta > 0) {
|
||||
await Request.sleep(backoffTimedelta)
|
||||
}
|
||||
// this.timeout = setTimeout ? setTimeout - backoffTimedelta : setTimeout
|
||||
return baseRequest(apiUrl, requestConfig)
|
||||
.then(res => res.json())
|
||||
|
||||
const requestTimeout = timeout ? timeout - backoffTimedelta : timeout
|
||||
return baseRequest(apiUrl, requestConfig, requestTimeout)
|
||||
.then(async (res) => {
|
||||
this.connectionError = null
|
||||
return res.json()
|
||||
})
|
||||
.catch(err => {
|
||||
// ConnectionError
|
||||
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() {
|
||||
if (!this.backoffTime) {
|
||||
return 0
|
||||
@ -85,17 +110,6 @@ export default class Request {
|
||||
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) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export default class Transport {
|
||||
this.connectionPool = []
|
||||
this.timeout = timeout
|
||||
// the maximum backoff time is 10 seconds
|
||||
this.maxBackoffTime = timeout ? timeout / 10 : 10000
|
||||
this.maxBackoffTime = timeout ? timeout / 2 : 10000
|
||||
nodes.forEach(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,
|
||||
// prefer the one with the smaller list index
|
||||
pickConnection() {
|
||||
if (this.connectionPool.length === 1) {
|
||||
return this.connectionPool[0]
|
||||
}
|
||||
let connection = this.connectionPool[0]
|
||||
|
||||
this.connectionPool.forEach(conn => {
|
||||
@ -58,7 +55,7 @@ export default class Transport {
|
||||
this.maxBackoffTime
|
||||
)
|
||||
const elapsed = Date.now() - startTime
|
||||
if (connection.backoffTime) {
|
||||
if (connection.backoffTime && this.timeout) {
|
||||
this.timeout -= elapsed
|
||||
} else {
|
||||
// No connection error, the response is valid
|
||||
@ -71,6 +68,6 @@ export default class Transport {
|
||||
const errorObject = {
|
||||
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 => {
|
||||
const path = 'http://localhost:9984/api/wrong/'
|
||||
const connection = new Connection(path, {}, 0)
|
||||
const connection = new Connection(path)
|
||||
const target = {
|
||||
message: 'HTTP Error: Requested page not reachable',
|
||||
status: '404 NOT FOUND',
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
|
||||
test('Pick connection with earliest backoff time', async t => {
|
||||
const path1 = 'http://localhost:9984/api/v1/'
|
||||
const path2 = 'http://localhost:9984/api/wrong/'
|
||||
const path2 = 'http://localhostwrong:9984/api/v1/'
|
||||
|
||||
// Reverse order
|
||||
const conn = new Connection([path2, path1])
|
||||
|
Loading…
Reference in New Issue
Block a user