Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit cf38aea

Browse files
lidelhugomrdias
authored andcommittedSep 10, 2019
fix: limit concurrent HTTP requests in browser (#2304)
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: libp2p/js-libp2p-delegated-content-routing#12
1 parent 3878f0f commit cf38aea

File tree

12 files changed

+88
-83
lines changed

12 files changed

+88
-83
lines changed
 

‎.aegir.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const preloadNode = MockPreloadNode.createNode()
1010
const echoServer = EchoServer.createServer()
1111

1212
module.exports = {
13-
bundlesize: { maxSize: '689kB' },
13+
bundlesize: { maxSize: '692kB' },
1414
webpack: {
1515
resolve: {
1616
mainFields: ['browser', 'main'],

‎package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"./src/core/runtime/add-from-fs-nodejs.js": "./src/core/runtime/add-from-fs-browser.js",
2020
"./src/core/runtime/config-nodejs.js": "./src/core/runtime/config-browser.js",
2121
"./src/core/runtime/dns-nodejs.js": "./src/core/runtime/dns-browser.js",
22-
"./src/core/runtime/fetch-nodejs.js": "./src/core/runtime/fetch-browser.js",
2322
"./src/core/runtime/libp2p-nodejs.js": "./src/core/runtime/libp2p-browser.js",
2423
"./src/core/runtime/libp2p-pubsub-routers-nodejs.js": "./src/core/runtime/libp2p-pubsub-routers-browser.js",
2524
"./src/core/runtime/preload-nodejs.js": "./src/core/runtime/preload-browser.js",
@@ -123,6 +122,8 @@
123122
"it-to-stream": "^0.1.1",
124123
"just-safe-set": "^2.1.0",
125124
"kind-of": "^6.0.2",
125+
"ky": "~0.13.0",
126+
"ky-universal": "~0.3.0",
126127
"libp2p": "~0.26.1",
127128
"libp2p-bootstrap": "~0.9.3",
128129
"libp2p-crypto": "~0.16.0",
@@ -151,7 +152,7 @@
151152
"multicodec": "~0.5.5",
152153
"multihashes": "~0.4.14",
153154
"multihashing-async": "~0.6.0",
154-
"node-fetch": "^2.3.0",
155+
"p-queue": "^6.1.0",
155156
"peer-book": "~0.9.0",
156157
"peer-id": "~0.12.3",
157158
"peer-info": "~0.15.0",

‎src/cli/commands/daemon.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const os = require('os')
44
const toUri = require('multiaddr-to-uri')
55
const { ipfsPathHelp } = require('../utils')
6+
const { isTest } = require('ipfs-utils/src/env')
67

78
module.exports = {
89
command: 'daemon',
@@ -27,7 +28,7 @@ module.exports = {
2728
})
2829
.option('enable-preload', {
2930
type: 'boolean',
30-
default: true
31+
default: !isTest // preload by default, unless in test env
3132
})
3233
},
3334

Original file line numberDiff line numberDiff line change
@@ -1,41 +1,22 @@
11
'use strict'
22

33
const { URL } = require('iso-url')
4-
const fetch = require('../../runtime/fetch-nodejs')
5-
6-
module.exports = (self) => {
7-
return async (url, options, callback) => {
8-
if (typeof options === 'function') {
9-
callback = options
10-
options = {}
11-
}
12-
13-
let files
14-
15-
try {
16-
const parsedUrl = new URL(url)
17-
const res = await fetch(url)
18-
19-
if (!res.ok) {
20-
throw new Error('unexpected status code: ' + res.status)
21-
}
22-
23-
// TODO: use res.body when supported
24-
const content = Buffer.from(await res.arrayBuffer())
25-
const path = decodeURIComponent(parsedUrl.pathname.split('/').pop())
26-
27-
files = await self.add({ content, path }, options)
28-
} catch (err) {
29-
if (callback) {
30-
return callback(err)
31-
}
32-
throw err
33-
}
4+
const nodeify = require('promise-nodeify')
5+
const { default: ky } = require('ky-universal')
6+
7+
module.exports = (ipfs) => {
8+
const addFromURL = async (url, opts = {}) => {
9+
const res = await ky.get(url)
10+
const path = decodeURIComponent(new URL(res.url).pathname.split('/').pop())
11+
const content = Buffer.from(await res.arrayBuffer())
12+
return ipfs.add({ content, path }, opts)
13+
}
3414

35-
if (callback) {
36-
callback(null, files)
15+
return (name, opts = {}, cb) => {
16+
if (typeof opts === 'function') {
17+
cb = opts
18+
opts = {}
3719
}
38-
39-
return files
20+
return nodeify(addFromURL(name, opts), cb)
4021
}
4122
}

‎src/core/config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const Multiaddr = require('multiaddr')
44
const mafmt = require('mafmt')
55
const { struct, superstruct } = require('superstruct')
6+
const { isTest } = require('ipfs-utils/src/env')
67

78
const { optional, union } = struct
89
const s = superstruct({
@@ -31,7 +32,7 @@ const configSchema = s({
3132
enabled: 'boolean?',
3233
addresses: optional(s(['multiaddr'])),
3334
interval: 'number?'
34-
}, { enabled: true, interval: 30 * 1000 }),
35+
}, { enabled: !isTest, interval: 30 * 1000 }),
3536
init: optional(union(['boolean', s({
3637
bits: 'number?',
3738
emptyRepo: 'boolean?',

‎src/core/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const defaultRepo = require('./runtime/repo-nodejs')
2727
const preload = require('./preload')
2828
const mfsPreload = require('./mfs-preload')
2929
const ipldOptions = require('./runtime/ipld-nodejs')
30+
const { isTest } = require('ipfs-utils/src/env')
3031

3132
/**
3233
* @typedef { import("./ipns/index") } IPNS
@@ -47,7 +48,7 @@ class IPFS extends EventEmitter {
4748
start: true,
4849
EXPERIMENTAL: {},
4950
preload: {
50-
enabled: true,
51+
enabled: !isTest, // preload by default, unless in test env
5152
addresses: [
5253
'/dnsaddr/node0.preload.ipfs.io/https',
5354
'/dnsaddr/node1.preload.ipfs.io/https'

‎src/core/runtime/dns-browser.js

+53-24
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,62 @@
1-
/* global self */
1+
/* eslint-env browser */
22
'use strict'
33

4-
module.exports = (domain, opts, callback) => {
4+
const TLRU = require('../../utils/tlru')
5+
const { default: PQueue } = require('p-queue')
6+
const { default: ky } = require('ky-universal')
7+
const nodeify = require('promise-nodeify')
8+
9+
// Avoid sending multiple queries for the same hostname by caching results
10+
const cache = new TLRU(1000)
11+
// TODO: /api/v0/dns does not return TTL yet: https://github.com/ipfs/go-ipfs/issues/5884
12+
// However we know browsers themselves cache DNS records for at least 1 minute,
13+
// which acts a provisional default ttl: https://stackoverflow.com/a/36917902/11518426
14+
const ttl = 60 * 1000
15+
16+
// browsers limit concurrent connections per host,
17+
// we don't want preload calls to exhaust the limit (~6)
18+
const httpQueue = new PQueue({ concurrency: 4 })
19+
20+
// Delegated HTTP resolver sending DNSLink queries to ipfs.io
21+
// TODO: replace hardcoded host with configurable DNS over HTTPS: https://github.com/ipfs/js-ipfs/issues/2212
22+
const api = ky.create({
23+
prefixUrl: 'https://ipfs.io/api/v0/',
24+
hooks: {
25+
afterResponse: [
26+
async (input, options, response) => {
27+
const query = new URL(response.url).search.slice(1)
28+
const json = await response.json()
29+
cache.set(query, json, ttl)
30+
}
31+
]
32+
}
33+
})
34+
35+
const ipfsPath = (response) => {
36+
if (response.Path) return response.Path
37+
throw new Error(response.Message)
38+
}
39+
40+
module.exports = (fqdn, opts = {}, cb) => {
541
if (typeof opts === 'function') {
6-
callback = opts
42+
cb = opts
743
opts = {}
844
}
45+
const resolveDnslink = async (fqdn, opts = {}) => {
46+
const searchParams = new URLSearchParams(opts)
47+
searchParams.set('arg', fqdn)
948

10-
opts = opts || {}
11-
12-
domain = encodeURIComponent(domain)
13-
let url = `https://ipfs.io/api/v0/dns?arg=${domain}`
49+
// try cache first
50+
const query = searchParams.toString()
51+
if (!opts.nocache && cache.has(query)) {
52+
const response = cache.get(query)
53+
return ipfsPath(response)
54+
}
1455

15-
Object.keys(opts).forEach(prop => {
16-
url += `&${encodeURIComponent(prop)}=${encodeURIComponent(opts[prop])}`
17-
})
56+
// fallback to delegated DNS resolver
57+
const response = await httpQueue.add(() => api.get('dns', { searchParams }).json())
58+
return ipfsPath(response)
59+
}
1860

19-
self.fetch(url, { mode: 'cors' })
20-
.then((response) => {
21-
return response.json()
22-
})
23-
.then((response) => {
24-
if (response.Path) {
25-
return callback(null, response.Path)
26-
} else {
27-
return callback(new Error(response.Message))
28-
}
29-
})
30-
.catch((error) => {
31-
callback(error)
32-
})
61+
return nodeify(resolveDnslink(fqdn, opts), cb)
3362
}

‎src/core/runtime/fetch-browser.js

-3
This file was deleted.

‎src/core/runtime/fetch-nodejs.js

-2
This file was deleted.

‎src/core/runtime/preload-browser.js

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
/* eslint-env browser */
22
'use strict'
33

4+
const { default: PQueue } = require('p-queue')
5+
const { default: ky } = require('ky-universal')
46
const debug = require('debug')
57

68
const log = debug('ipfs:preload')
79
log.error = debug('ipfs:preload:error')
810

11+
// browsers limit concurrent connections per host,
12+
// we don't want preload calls to exhaust the limit (~6)
13+
const httpQueue = new PQueue({ concurrency: 4 })
14+
915
module.exports = function preload (url, callback) {
1016
log(url)
1117

1218
const controller = new AbortController()
1319
const signal = controller.signal
20+
const cb = () => setImmediate(callback) // https://github.com/ipfs/js-ipfs/pull/2304#discussion_r320700893
1421

15-
fetch(url, { signal })
16-
.then(res => {
17-
if (!res.ok) {
18-
log.error('failed to preload', url, res.status, res.statusText)
19-
throw new Error(`failed to preload ${url}`)
20-
}
21-
return res.text()
22-
})
23-
.then(() => callback())
24-
.catch(callback)
22+
httpQueue.add(() => ky.get(url, { signal })).then(cb, cb)
2523

2624
return {
2725
cancel: () => controller.abort()

‎src/utils/tlru.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ class TLRU {
3333
this.lru.remove(key)
3434
return undefined
3535
}
36+
return value.value
3637
}
37-
return value.value
38+
return undefined
3839
}
3940

4041
/**

‎test/core/interface.spec.js

-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ describe('interface-ipfs-core tests', function () {
6464
}, {
6565
name: 'addFromFs',
6666
reason: 'Not designed to run in the browser'
67-
}, {
68-
name: 'addFromURL',
69-
reason: 'Not designed to run in the browser'
7067
}]
7168
})
7269

0 commit comments

Comments
 (0)
This repository has been archived.