From 72045f4aba1e63cce118092633e7eb4080fcfaaf Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 26 Jul 2019 16:43:26 +0200 Subject: [PATCH] fix: limit concurrent HTTP requests in browser Adds limit of concurrent HTTP requests sent to remote API by dns and preload calls when running in web browser contexts. Browsers limit connections per host (~6). This change mitigates the problem of expensive and long running calls of one type exhausting connection pool for other uses. It additionally limits the number of DNS lookup calls by introducing time-bound cache with eviction rules following what browser already do. This is similar to: https://github.com/libp2p/js-libp2p-delegated-content-routing/issues/12 License: MIT Signed-off-by: Marcin Rataj --- package.json | 1 + src/core/runtime/dns-browser.js | 44 ++++++++++++++++++++++------- src/core/runtime/preload-browser.js | 8 +++++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 0b87ec4e7b..3f8364455e 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "multihashes": "~0.4.14", "multihashing-async": "~0.6.0", "node-fetch": "^2.3.0", + "p-queue": "^6.1.0", "peer-book": "~0.9.0", "peer-id": "~0.12.3", "peer-info": "~0.15.0", diff --git a/src/core/runtime/dns-browser.js b/src/core/runtime/dns-browser.js index 74d0a14cdb..7b2725e491 100644 --- a/src/core/runtime/dns-browser.js +++ b/src/core/runtime/dns-browser.js @@ -1,33 +1,57 @@ -/* global self */ +/* eslint-env browser */ 'use strict' +const TLRU = require('../../utils/tlru') +const { default: PQueue } = require('p-queue') + +// Avoid sending multiple queries for the same hostname by caching results +const cache = new TLRU(1000) +// TODO: /api/v0/dns does not return TTL yet: https://github.com/ipfs/go-ipfs/issues/5884 +// However we know browsers themselves cache DNS records for at least 1 minute, +// which acts a provisional default ttl: https://stackoverflow.com/a/36917902/11518426 +const ttl = 60 * 1000 + +// browsers limit concurrent connections per host, +// we don't want preload calls to exhaust the limit (~6) +const _httpQueue = new PQueue({ concurrency: 4 }) + +function unpackResponse (domain, response, callback) { + if (response.Path) { + return callback(null, response.Path) + } else { + const err = new Error(response.Message) + return callback(err) + } +} + module.exports = (domain, opts, callback) => { if (typeof opts === 'function') { callback = opts opts = {} } - opts = opts || {} - domain = encodeURIComponent(domain) - let url = `https://ipfs.io/api/v0/dns?arg=${domain}` + if (cache.has(domain)) { + const response = cache.get(domain) + return unpackResponse(domain, response, callback) + } + + let url = `https://ipfs.io/api/v0/dns?arg=${domain}` Object.keys(opts).forEach(prop => { url += `&${encodeURIComponent(prop)}=${encodeURIComponent(opts[prop])}` }) - self.fetch(url, { mode: 'cors' }) + _httpQueue.add(() => fetch(url, { mode: 'cors' }) .then((response) => { return response.json() }) .then((response) => { - if (response.Path) { - return callback(null, response.Path) - } else { - return callback(new Error(response.Message)) - } + cache.set(domain, response, ttl) + return unpackResponse(domain, response, callback) }) .catch((error) => { callback(error) }) + ) } diff --git a/src/core/runtime/preload-browser.js b/src/core/runtime/preload-browser.js index 81407f483f..d24c7f3eff 100644 --- a/src/core/runtime/preload-browser.js +++ b/src/core/runtime/preload-browser.js @@ -1,18 +1,23 @@ /* eslint-env browser */ 'use strict' +const { default: PQueue } = require('p-queue') const debug = require('debug') const log = debug('ipfs:preload') log.error = debug('ipfs:preload:error') +// browsers limit concurrent connections per host, +// we don't want preload calls to exhaust the limit (~6) +const _httpQueue = new PQueue({ concurrency: 4 }) + module.exports = function preload (url, callback) { log(url) const controller = new AbortController() const signal = controller.signal - fetch(url, { signal }) + _httpQueue.add(() => fetch(url, { signal }) .then(res => { if (!res.ok) { log.error('failed to preload', url, res.status, res.statusText) @@ -22,6 +27,7 @@ module.exports = function preload (url, callback) { }) .then(() => callback()) .catch(callback) + ).catch(callback) return { cancel: () => controller.abort()