handle timeout for each request

This commit is contained in:
manolodewiner 2018-08-29 16:39:15 +02:00
parent b30578d9ab
commit 11892a1f6b
6 changed files with 82 additions and 54 deletions

View File

@ -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)
}
}

View File

@ -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)
}
/**

View File

@ -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))
}

View File

@ -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
}
}

View File

@ -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',

View File

@ -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])