Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 53 additions & 36 deletions lib/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@
'use strict';

const {
Array,
ArrayPrototypePush,
ObjectDefineProperties,
ObjectDefineProperty,
SafeMap,
Symbol,
} = primordials;

const cares = internalBinding('cares_wrap');
const { isIP } = require('internal/net');
const { customPromisifyArgs } = require('internal/util');
const { AsyncResource } = require('async_hooks');
const {
DNSException,
codes: {
Expand Down Expand Up @@ -105,36 +109,12 @@ const {

let promises = null; // Lazy loaded

function onlookup(err, addresses) {
if (err) {
return this.callback(new DNSException(err, 'getaddrinfo', this.hostname));
}
this.callback(null, addresses[0], this.family || isIP(addresses[0]));
if (this[kPerfHooksDnsLookupContext] && hasObserver('dns')) {
stopPerf(this, kPerfHooksDnsLookupContext, { detail: { addresses } });
}
}


function onlookupall(err, addresses) {
if (err) {
return this.callback(new DNSException(err, 'getaddrinfo', this.hostname));
}

const family = this.family;
for (let i = 0; i < addresses.length; i++) {
const addr = addresses[i];
addresses[i] = {
address: addr,
family: family || isIP(addr),
};
}

this.callback(null, addresses);
if (this[kPerfHooksDnsLookupContext] && hasObserver('dns')) {
stopPerf(this, kPerfHooksDnsLookupContext, { detail: { addresses } });
}
}
// Map of in-flight getaddrinfo requests for lookup coalescing.
// Key: `${hostname}\0${family}\0${hints}\0${order}`
// Value: Array<{ callback, all }>
// When multiple callers request the same lookup concurrently, only one
// getaddrinfo call is dispatched to the libuv threadpool.
const inflightLookups = new SafeMap();


// Easy DNS A/AAAA look up
Expand Down Expand Up @@ -213,12 +193,6 @@ function lookup(hostname, options, callback) {
return {};
}

const req = new GetAddrInfoReqWrap();
req.callback = callback;
req.family = family;
req.hostname = hostname;
req.oncomplete = all ? onlookupall : onlookup;

let order = DNS_ORDER_VERBATIM;

if (dnsOrder === 'ipv4first') {
Expand All @@ -227,10 +201,53 @@ function lookup(hostname, options, callback) {
order = DNS_ORDER_IPV6_FIRST;
}

const key = `${hostname}\0${family}\0${hints}\0${order}`;
const waiter = { callback: AsyncResource.bind(callback), all };

let inflight = inflightLookups.get(key);
if (inflight) {
// Piggyback on existing in-flight request.
ArrayPrototypePush(inflight, waiter);
return {};
}

// First request for this key - dispatch to libuv.
inflight = [waiter];
inflightLookups.set(key, inflight);

const req = new GetAddrInfoReqWrap();
req.family = family;
req.hostname = hostname;
req.oncomplete = function(errCode, addresses) {
inflightLookups.delete(key);
const waiters = inflight;
for (let i = 0; i < waiters.length; i++) {
const w = waiters[i];
if (errCode) {
w.callback(new DNSException(errCode, 'getaddrinfo', hostname));
} else if (w.all) {
const result = new Array(addresses.length);
for (let j = 0; j < addresses.length; j++) {
result[j] = {
address: addresses[j],
family: family || isIP(addresses[j]),
};
}
w.callback(null, result);
} else {
w.callback(null, addresses[0], family || isIP(addresses[0]));
}
}
if (this[kPerfHooksDnsLookupContext] && hasObserver('dns')) {
stopPerf(this, kPerfHooksDnsLookupContext, { detail: { addresses } });
}
};

const err = cares.getaddrinfo(
req, hostname, family, hints, order,
);
if (err) {
inflightLookups.delete(key);
process.nextTick(callback, new DNSException(err, 'getaddrinfo', hostname));
return {};
}
Expand Down
128 changes: 65 additions & 63 deletions lib/internal/dns/promises.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
'use strict';
const {
Array,
ArrayPrototypeMap,
FunctionPrototypeCall,
ObjectDefineProperty,
Promise,
PromiseReject,
PromiseResolve,
SafeMap,
Symbol,
} = primordials;

Expand Down Expand Up @@ -82,41 +86,14 @@ const {
stopPerf,
} = require('internal/perf/observe');

function onlookup(err, addresses) {
if (err) {
this.reject(new DNSException(err, 'getaddrinfo', this.hostname));
return;
}

const family = this.family || isIP(addresses[0]);
this.resolve({ address: addresses[0], family });
if (this[kPerfHooksDnsLookupContext] && hasObserver('dns')) {
stopPerf(this, kPerfHooksDnsLookupContext, { detail: { addresses } });
}
}

function onlookupall(err, addresses) {
if (err) {
this.reject(new DNSException(err, 'getaddrinfo', this.hostname));
return;
}

const family = this.family;

for (let i = 0; i < addresses.length; i++) {
const address = addresses[i];

addresses[i] = {
address,
family: family || isIP(addresses[i]),
};
}

this.resolve(addresses);
if (this[kPerfHooksDnsLookupContext] && hasObserver('dns')) {
stopPerf(this, kPerfHooksDnsLookupContext, { detail: { addresses } });
}
}
// Map of in-flight getaddrinfo requests for lookup coalescing.
// Key: `${hostname}\0${family}\0${hints}\0${order}`
// Value: Promise<{ err: number|null, addresses: string[]|null }>
// When multiple callers request the same lookup concurrently, only one
// getaddrinfo call is dispatched to the libuv threadpool. All callers
// share the result, avoiding threadpool starvation under sustained DNS
// failure where each getaddrinfo blocks for many seconds.
const inflightLookups = new SafeMap();

/**
* Creates a promise that resolves with the IP address of the given hostname.
Expand All @@ -132,41 +109,48 @@ function onlookupall(err, addresses) {
* @property {0 | 4 | 6} family - The IP address type. 4 for IPv4 or 6 for IPv6, or 0 (for both).
*/
function createLookupPromise(family, hostname, all, hints, dnsOrder) {
return new Promise((resolve, reject) => {
if (!hostname) {
reject(new ERR_INVALID_ARG_VALUE('hostname', hostname,
'must be a non-empty string'));
return;
}
if (!hostname) {
return PromiseReject(
new ERR_INVALID_ARG_VALUE('hostname', hostname,
'must be a non-empty string'));
}

const matchedFamily = isIP(hostname);
const matchedFamily = isIP(hostname);
if (matchedFamily !== 0) {
const result = { address: hostname, family: matchedFamily };
return PromiseResolve(all ? [result] : result);
}

if (matchedFamily !== 0) {
const result = { address: hostname, family: matchedFamily };
resolve(all ? [result] : result);
return;
}
let order = DNS_ORDER_VERBATIM;
if (dnsOrder === 'ipv4first') {
order = DNS_ORDER_IPV4_FIRST;
} else if (dnsOrder === 'ipv6first') {
order = DNS_ORDER_IPV6_FIRST;
}

const req = new GetAddrInfoReqWrap();
const key = `${hostname}\0${family}\0${hints}\0${order}`;

let rawPromise = inflightLookups.get(key);
if (!rawPromise) {
let resolveRaw;
rawPromise = new Promise((resolve) => { resolveRaw = resolve; });
inflightLookups.set(key, rawPromise);

const req = new GetAddrInfoReqWrap();
req.family = family;
req.hostname = hostname;
req.oncomplete = all ? onlookupall : onlookup;
req.resolve = resolve;
req.reject = reject;

let order = DNS_ORDER_VERBATIM;

if (dnsOrder === 'ipv4first') {
order = DNS_ORDER_IPV4_FIRST;
} else if (dnsOrder === 'ipv6first') {
order = DNS_ORDER_IPV6_FIRST;
}

const err = getaddrinfo(req, hostname, family, hints, order);
req.oncomplete = function(errCode, addresses) {
inflightLookups.delete(key);
resolveRaw({ err: errCode, addresses });
if (this[kPerfHooksDnsLookupContext] && hasObserver('dns')) {
stopPerf(this, kPerfHooksDnsLookupContext, { detail: { addresses } });
}
};

if (err) {
reject(new DNSException(err, 'getaddrinfo', hostname));
const errCode = getaddrinfo(req, hostname, family, hints, order);
if (errCode) {
inflightLookups.delete(key);
resolveRaw({ err: errCode, addresses: null });
} else if (hasObserver('dns')) {
const detail = {
hostname,
Expand All @@ -177,6 +161,24 @@ function createLookupPromise(family, hostname, all, hints, dnsOrder) {
};
startPerf(req, kPerfHooksDnsLookupContext, { type: 'dns', name: 'lookup', detail });
}
}

// Each caller post-processes the shared raw result independently.
return rawPromise.then(({ err, addresses }) => {
if (err) {
throw new DNSException(err, 'getaddrinfo', hostname);
}
if (all) {
const result = new Array(addresses.length);
for (let i = 0; i < addresses.length; i++) {
result[i] = {
address: addresses[i],
family: family || isIP(addresses[i]),
};
}
return result;
}
return { address: addresses[0], family: family || isIP(addresses[0]) };
});
}

Expand Down
Loading
Loading