Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a fallback to dns.lookup(...) #15

Merged
merged 12 commits into from
Apr 27, 2020
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,32 @@ Options used to cache the DNS lookups.
Type: `number`<br>
Default: `Infinity`

Limits the cache time (TTL in seconds).
The maximum lifetime of the entries received from the specifed DNS server (TTL in seconds).

If set to `0`, it will make a new DNS query each time.

##### options.fallbackTtl

Type: `number`<br>
Default: `1`

The lifetime of the entries received from the OS (TTL in seconds).

**Note**: This option is independent, `options.maxTtl` does not affect this.

**Pro Tip**: This shouldn't be lower than your DNS server response time in order to prevent bottlenecks.

##### options.errorTtl

Type: `number`<br>
Default: `0.150`

The time how long it needs to remember failed queries (TTL in seconds).

**Note**: This option is independent, `options.maxTtl` does not affect this.

**Pro Tip**: This shouldn't be lower than your DNS server response time in order to prevent bottlenecks.

##### options.resolver

Type: `dns.Resolver | dns.promises.Resolver`<br>
Expand Down Expand Up @@ -146,16 +168,6 @@ Type: `object`

The same as the [`dns.lookup(…)`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) options.

##### options.throwNotFound

Type: `boolean`<br>
Default: `true`

If set to `false` and it gets no match, it will return `undefined`.
If set to `true` and it gets no match, it will throw `ENOTFOUND` error.

**Note**: This option is meant **only** for the asynchronous implementation! The callback version will always pass an error if no match found.

#### query(hostname)

An asynchronous function which returns cached DNS lookup entries.<br>
Expand Down
6 changes: 3 additions & 3 deletions benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ const lookupOptionsADDRCONFIG = {
hints: dns.ADDRCONFIG
};

const query = 'example.com';
const query = 'short';
szmarczak marked this conversation as resolved.
Show resolved Hide resolved

suite.add('CacheableLookup#lookupAsync', deferred => {
// eslint-disable-next-line promise/prefer-await-to-then
cacheable.lookupAsync(query).then(() => deferred.resolve());
}, options).add('CacheableLookup#lookupAsync.all', deferred => {
}, options)/*.add('CacheableLookup#lookupAsync.all', deferred => {
// eslint-disable-next-line promise/prefer-await-to-then
cacheable.lookupAsync(query, lookupOptions).then(() => deferred.resolve());
}, options).add('CacheableLookup#lookupAsync.all.ADDRCONFIG', deferred => {
Expand All @@ -54,7 +54,7 @@ suite.add('CacheableLookup#lookupAsync', deferred => {
dns.lookup(query, lookupOptions, () => deferred.resolve());
}, options).add('dns#lookup.all.ADDRCONFIG', deferred => {
dns.lookup(query, lookupOptionsADDRCONFIG, () => deferred.resolve());
}, options).on('cycle', event => {
}, options)*/.on('cycle', event => {
console.log(String(event.target));
}).on('complete', function () {
console.log(`Fastest is ${this.filter('fastest').map('name')}`);
Expand Down
72 changes: 49 additions & 23 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ const {promisify} = require('util');
const os = require('os');
const HostsResolver = require('./hosts-resolver');

const {Resolver: AsyncResolver} = dnsPromises;
const {
Resolver: AsyncResolver,
lookup: dnsPromiseLookup
} = dnsPromises;

const kCacheableLookupCreateConnection = Symbol('cacheableLookupCreateConnection');
const kCacheableLookupInstance = Symbol('cacheableLookupInstance');
Expand Down Expand Up @@ -95,9 +98,13 @@ class CacheableLookup {
cache = new TTLMap(),
maxTtl = Infinity,
resolver = new AsyncResolver(),
fallbackTtl = 1,
errorTtl = 0.150,
customHostsPath
} = {}) {
this.maxTtl = maxTtl;
this.fallbackTtl = fallbackTtl;
this.errorTtl = errorTtl;

this._cache = cache;
this._resolver = resolver;
Expand Down Expand Up @@ -126,14 +133,14 @@ class CacheableLookup {
return this._resolver.getServers();
}

lookup(hostname, options, callback) {
lookup(hostname, options = {}, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}

// eslint-disable-next-line promise/prefer-await-to-then
this.lookupAsync(hostname, options, true).then(result => {
this.lookupAsync(hostname, options).then(result => {
if (options.all) {
callback(null, result);
} else {
Expand All @@ -142,7 +149,7 @@ class CacheableLookup {
}).catch(callback);
}

async lookupAsync(hostname, options = {}, throwNotFound = undefined) {
async lookupAsync(hostname, options = {}) {
let cached = await this.query(hostname);

if (options.family === 6) {
Expand All @@ -162,16 +169,6 @@ class CacheableLookup {
cached = cached.filter(entry => entry.family === 6 ? _iface.has6 : _iface.has4);
}

if (cached.length === 0) {
if (throwNotFound || options.throwNotFound !== false) {
const error = new Error(`ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
error.hostname = hostname;

throw error;
}
}

if (options.all) {
return cached;
}
Expand All @@ -180,16 +177,26 @@ class CacheableLookup {
return cached[0];
}

return this._getEntry(cached);
return this._getEntry(cached, hostname);
}

async query(hostname) {
this.tick();

let cached = await this._hostsResolver.get(hostname) || await this._cache.get(hostname);

if (!cached || cached.length === 0) {
if (!cached) {
cached = await this.queryAndCache(hostname);
}

if (cached.length === 0) {
const error = new Error(`ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
error.hostname = hostname;

throw error;
}

cached = cached.map(entry => {
return {...entry};
});
Expand All @@ -198,6 +205,7 @@ class CacheableLookup {
}

async queryAndCache(hostname) {
// We could make an ANY query, but DNS servers may reject that.
const [As, AAAAs] = await Promise.all([this._resolve4(hostname, ttl).catch(() => []), this._resolve6(hostname, ttl).catch(() => [])]);

let cacheTtl = 0;
Expand All @@ -208,6 +216,7 @@ class CacheableLookup {
entry.family = 4;
entry.expires = now + (entry.ttl * 1000);

// Is TTL the same for all entries?
cacheTtl = Math.max(cacheTtl, entry.ttl);
}
}
Expand All @@ -217,13 +226,33 @@ class CacheableLookup {
entry.family = 6;
entry.expires = now + (entry.ttl * 1000);

// Is TTL the same for all entries?
cacheTtl = Math.max(cacheTtl, entry.ttl);
}
}

const entries = [...(As || []), ...(AAAAs || [])];
let entries = [...(As || []), ...(AAAAs || [])];

if (entries.length === 0) {
try {
entries = await dnsPromiseLookup(hostname, {
all: true
});

for (const entry of entries) {
entry.ttl = this.fallbackTtl;
entry.expires = now + (entry.ttl * 1000);
}

cacheTtl = this.fallbackTtl * 1000;
} catch (error) {
await this._cache.set(hostname, [], this.errorTtl * 1000);

cacheTtl = Math.min(this.maxTtl, cacheTtl) * 1000;
throw error;
}
} else {
cacheTtl = Math.min(this.maxTtl, cacheTtl) * 1000;
}

if (this.maxTtl > 0 && cacheTtl > 0) {
await this._cache.set(hostname, entries, cacheTtl);
Expand All @@ -232,7 +261,7 @@ class CacheableLookup {
return entries;
}

_getEntry(entries) {
_getEntry(entries, hostname) {
return entries[Math.floor(Math.random() * entries.length)];
}

Expand All @@ -241,7 +270,7 @@ class CacheableLookup {
return;
}

if (this._cache instanceof TTLMap) {
if (this._cache instanceof TTLMap && this._cache.size) {
const now = Date.now();

for (const [hostname, expiry] of this._cache.expiries) {
Expand Down Expand Up @@ -273,9 +302,6 @@ class CacheableLookup {
agent.createConnection = (options, callback) => {
if (!('lookup' in options)) {
options.lookup = this.lookup;

// Make sure the database is up to date
this.tick();
}

return agent[kCacheableLookupCreateConnection](options, callback);
Expand Down