Initial Commit
This commit is contained in:
412
node_modules/@grpc/grpc-js/src/resolver-dns.ts
generated
vendored
Normal file
412
node_modules/@grpc/grpc-js/src/resolver-dns.ts
generated
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* Copyright 2019 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Resolver,
|
||||
ResolverListener,
|
||||
registerResolver,
|
||||
registerDefaultScheme,
|
||||
} from './resolver';
|
||||
import * as dns from 'dns';
|
||||
import * as util from 'util';
|
||||
import { extractAndSelectServiceConfig, ServiceConfig } from './service-config';
|
||||
import { Status } from './constants';
|
||||
import { StatusObject } from './call-interface';
|
||||
import { Metadata } from './metadata';
|
||||
import * as logging from './logging';
|
||||
import { LogVerbosity } from './constants';
|
||||
import { SubchannelAddress, TcpSubchannelAddress } from './subchannel-address';
|
||||
import { GrpcUri, uriToString, splitHostPort } from './uri-parser';
|
||||
import { isIPv6, isIPv4 } from 'net';
|
||||
import { ChannelOptions } from './channel-options';
|
||||
import { BackoffOptions, BackoffTimeout } from './backoff-timeout';
|
||||
|
||||
const TRACER_NAME = 'dns_resolver';
|
||||
|
||||
function trace(text: string): void {
|
||||
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default TCP port to connect to if not explicitly specified in the target.
|
||||
*/
|
||||
export const DEFAULT_PORT = 443;
|
||||
|
||||
const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30_000;
|
||||
|
||||
const resolveTxtPromise = util.promisify(dns.resolveTxt);
|
||||
const dnsLookupPromise = util.promisify(dns.lookup);
|
||||
|
||||
/**
|
||||
* Merge any number of arrays into a single alternating array
|
||||
* @param arrays
|
||||
*/
|
||||
function mergeArrays<T>(...arrays: T[][]): T[] {
|
||||
const result: T[] = [];
|
||||
for (
|
||||
let i = 0;
|
||||
i <
|
||||
Math.max.apply(
|
||||
null,
|
||||
arrays.map(array => array.length)
|
||||
);
|
||||
i++
|
||||
) {
|
||||
for (const array of arrays) {
|
||||
if (i < array.length) {
|
||||
result.push(array[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolver implementation that handles DNS names and IP addresses.
|
||||
*/
|
||||
class DnsResolver implements Resolver {
|
||||
private readonly ipResult: SubchannelAddress[] | null;
|
||||
private readonly dnsHostname: string | null;
|
||||
private readonly port: number | null;
|
||||
/**
|
||||
* Minimum time between resolutions, measured as the time between starting
|
||||
* successive resolution requests. Only applies to successful resolutions.
|
||||
* Failures are handled by the backoff timer.
|
||||
*/
|
||||
private readonly minTimeBetweenResolutionsMs: number;
|
||||
private pendingLookupPromise: Promise<dns.LookupAddress[]> | null = null;
|
||||
private pendingTxtPromise: Promise<string[][]> | null = null;
|
||||
private latestLookupResult: TcpSubchannelAddress[] | null = null;
|
||||
private latestServiceConfig: ServiceConfig | null = null;
|
||||
private latestServiceConfigError: StatusObject | null = null;
|
||||
private percentage: number;
|
||||
private defaultResolutionError: StatusObject;
|
||||
private backoff: BackoffTimeout;
|
||||
private continueResolving = false;
|
||||
private nextResolutionTimer: NodeJS.Timeout;
|
||||
private isNextResolutionTimerRunning = false;
|
||||
private isServiceConfigEnabled = true;
|
||||
private returnedIpResult = false;
|
||||
constructor(
|
||||
private target: GrpcUri,
|
||||
private listener: ResolverListener,
|
||||
channelOptions: ChannelOptions
|
||||
) {
|
||||
trace('Resolver constructed for target ' + uriToString(target));
|
||||
const hostPort = splitHostPort(target.path);
|
||||
if (hostPort === null) {
|
||||
this.ipResult = null;
|
||||
this.dnsHostname = null;
|
||||
this.port = null;
|
||||
} else {
|
||||
if (isIPv4(hostPort.host) || isIPv6(hostPort.host)) {
|
||||
this.ipResult = [
|
||||
{
|
||||
host: hostPort.host,
|
||||
port: hostPort.port ?? DEFAULT_PORT,
|
||||
},
|
||||
];
|
||||
this.dnsHostname = null;
|
||||
this.port = null;
|
||||
} else {
|
||||
this.ipResult = null;
|
||||
this.dnsHostname = hostPort.host;
|
||||
this.port = hostPort.port ?? DEFAULT_PORT;
|
||||
}
|
||||
}
|
||||
this.percentage = Math.random() * 100;
|
||||
|
||||
if (channelOptions['grpc.service_config_disable_resolution'] === 1) {
|
||||
this.isServiceConfigEnabled = false;
|
||||
}
|
||||
|
||||
this.defaultResolutionError = {
|
||||
code: Status.UNAVAILABLE,
|
||||
details: `Name resolution failed for target ${uriToString(this.target)}`,
|
||||
metadata: new Metadata(),
|
||||
};
|
||||
|
||||
const backoffOptions: BackoffOptions = {
|
||||
initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
|
||||
maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
|
||||
};
|
||||
|
||||
this.backoff = new BackoffTimeout(() => {
|
||||
if (this.continueResolving) {
|
||||
this.startResolutionWithBackoff();
|
||||
}
|
||||
}, backoffOptions);
|
||||
this.backoff.unref();
|
||||
|
||||
this.minTimeBetweenResolutionsMs =
|
||||
channelOptions['grpc.dns_min_time_between_resolutions_ms'] ??
|
||||
DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS;
|
||||
this.nextResolutionTimer = setTimeout(() => {}, 0);
|
||||
clearTimeout(this.nextResolutionTimer);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the target is an IP address, just provide that address as a result.
|
||||
* Otherwise, initiate A, AAAA, and TXT lookups
|
||||
*/
|
||||
private startResolution() {
|
||||
if (this.ipResult !== null) {
|
||||
if (!this.returnedIpResult) {
|
||||
trace('Returning IP address for target ' + uriToString(this.target));
|
||||
setImmediate(() => {
|
||||
this.listener.onSuccessfulResolution(
|
||||
this.ipResult!,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
});
|
||||
this.returnedIpResult = true;
|
||||
}
|
||||
this.backoff.stop();
|
||||
this.backoff.reset();
|
||||
this.stopNextResolutionTimer();
|
||||
return;
|
||||
}
|
||||
if (this.dnsHostname === null) {
|
||||
trace('Failed to parse DNS address ' + uriToString(this.target));
|
||||
setImmediate(() => {
|
||||
this.listener.onError({
|
||||
code: Status.UNAVAILABLE,
|
||||
details: `Failed to parse DNS address ${uriToString(this.target)}`,
|
||||
metadata: new Metadata(),
|
||||
});
|
||||
});
|
||||
this.stopNextResolutionTimer();
|
||||
} else {
|
||||
if (this.pendingLookupPromise !== null) {
|
||||
return;
|
||||
}
|
||||
trace('Looking up DNS hostname ' + this.dnsHostname);
|
||||
/* We clear out latestLookupResult here to ensure that it contains the
|
||||
* latest result since the last time we started resolving. That way, the
|
||||
* TXT resolution handler can use it, but only if it finishes second. We
|
||||
* don't clear out any previous service config results because it's
|
||||
* better to use a service config that's slightly out of date than to
|
||||
* revert to an effectively blank one. */
|
||||
this.latestLookupResult = null;
|
||||
const hostname: string = this.dnsHostname;
|
||||
/* We lookup both address families here and then split them up later
|
||||
* because when looking up a single family, dns.lookup outputs an error
|
||||
* if the name exists but there are no records for that family, and that
|
||||
* error is indistinguishable from other kinds of errors */
|
||||
this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true });
|
||||
this.pendingLookupPromise.then(
|
||||
addressList => {
|
||||
if (this.pendingLookupPromise === null) {
|
||||
return;
|
||||
}
|
||||
this.pendingLookupPromise = null;
|
||||
this.backoff.reset();
|
||||
this.backoff.stop();
|
||||
const ip4Addresses: dns.LookupAddress[] = addressList.filter(
|
||||
addr => addr.family === 4
|
||||
);
|
||||
const ip6Addresses: dns.LookupAddress[] = addressList.filter(
|
||||
addr => addr.family === 6
|
||||
);
|
||||
this.latestLookupResult = mergeArrays(ip6Addresses, ip4Addresses).map(
|
||||
addr => ({ host: addr.address, port: +this.port! })
|
||||
);
|
||||
const allAddressesString: string =
|
||||
'[' +
|
||||
this.latestLookupResult
|
||||
.map(addr => addr.host + ':' + addr.port)
|
||||
.join(',') +
|
||||
']';
|
||||
trace(
|
||||
'Resolved addresses for target ' +
|
||||
uriToString(this.target) +
|
||||
': ' +
|
||||
allAddressesString
|
||||
);
|
||||
if (this.latestLookupResult.length === 0) {
|
||||
this.listener.onError(this.defaultResolutionError);
|
||||
return;
|
||||
}
|
||||
/* If the TXT lookup has not yet finished, both of the last two
|
||||
* arguments will be null, which is the equivalent of getting an
|
||||
* empty TXT response. When the TXT lookup does finish, its handler
|
||||
* can update the service config by using the same address list */
|
||||
this.listener.onSuccessfulResolution(
|
||||
this.latestLookupResult,
|
||||
this.latestServiceConfig,
|
||||
this.latestServiceConfigError,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
if (this.pendingLookupPromise === null) {
|
||||
return;
|
||||
}
|
||||
trace(
|
||||
'Resolution error for target ' +
|
||||
uriToString(this.target) +
|
||||
': ' +
|
||||
(err as Error).message
|
||||
);
|
||||
this.pendingLookupPromise = null;
|
||||
this.stopNextResolutionTimer();
|
||||
this.listener.onError(this.defaultResolutionError);
|
||||
}
|
||||
);
|
||||
/* If there already is a still-pending TXT resolution, we can just use
|
||||
* that result when it comes in */
|
||||
if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) {
|
||||
/* We handle the TXT query promise differently than the others because
|
||||
* the name resolution attempt as a whole is a success even if the TXT
|
||||
* lookup fails */
|
||||
this.pendingTxtPromise = resolveTxtPromise(hostname);
|
||||
this.pendingTxtPromise.then(
|
||||
txtRecord => {
|
||||
if (this.pendingTxtPromise === null) {
|
||||
return;
|
||||
}
|
||||
this.pendingTxtPromise = null;
|
||||
try {
|
||||
this.latestServiceConfig = extractAndSelectServiceConfig(
|
||||
txtRecord,
|
||||
this.percentage
|
||||
);
|
||||
} catch (err) {
|
||||
this.latestServiceConfigError = {
|
||||
code: Status.UNAVAILABLE,
|
||||
details: `Parsing service config failed with error ${
|
||||
(err as Error).message
|
||||
}`,
|
||||
metadata: new Metadata(),
|
||||
};
|
||||
}
|
||||
if (this.latestLookupResult !== null) {
|
||||
/* We rely here on the assumption that calling this function with
|
||||
* identical parameters will be essentialy idempotent, and calling
|
||||
* it with the same address list and a different service config
|
||||
* should result in a fast and seamless switchover. */
|
||||
this.listener.onSuccessfulResolution(
|
||||
this.latestLookupResult,
|
||||
this.latestServiceConfig,
|
||||
this.latestServiceConfigError,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
/* If TXT lookup fails we should do nothing, which means that we
|
||||
* continue to use the result of the most recent successful lookup,
|
||||
* or the default null config object if there has never been a
|
||||
* successful lookup. We do not set the latestServiceConfigError
|
||||
* here because that is specifically used for response validation
|
||||
* errors. We still need to handle this error so that it does not
|
||||
* bubble up as an unhandled promise rejection. */
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startNextResolutionTimer() {
|
||||
clearTimeout(this.nextResolutionTimer);
|
||||
this.nextResolutionTimer = setTimeout(() => {
|
||||
this.stopNextResolutionTimer();
|
||||
if (this.continueResolving) {
|
||||
this.startResolutionWithBackoff();
|
||||
}
|
||||
}, this.minTimeBetweenResolutionsMs).unref?.();
|
||||
this.isNextResolutionTimerRunning = true;
|
||||
}
|
||||
|
||||
private stopNextResolutionTimer() {
|
||||
clearTimeout(this.nextResolutionTimer);
|
||||
this.isNextResolutionTimerRunning = false;
|
||||
}
|
||||
|
||||
private startResolutionWithBackoff() {
|
||||
if (this.pendingLookupPromise === null) {
|
||||
this.continueResolving = false;
|
||||
this.backoff.runOnce();
|
||||
this.startNextResolutionTimer();
|
||||
this.startResolution();
|
||||
}
|
||||
}
|
||||
|
||||
updateResolution() {
|
||||
/* If there is a pending lookup, just let it finish. Otherwise, if the
|
||||
* nextResolutionTimer or backoff timer is running, set the
|
||||
* continueResolving flag to resolve when whichever of those timers
|
||||
* fires. Otherwise, start resolving immediately. */
|
||||
if (this.pendingLookupPromise === null) {
|
||||
if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) {
|
||||
if (this.isNextResolutionTimerRunning) {
|
||||
trace('resolution update delayed by "min time between resolutions" rate limit');
|
||||
} else {
|
||||
trace('resolution update delayed by backoff timer until ' + this.backoff.getEndTime().toISOString());
|
||||
}
|
||||
this.continueResolving = true;
|
||||
} else {
|
||||
this.startResolutionWithBackoff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the resolver to the same state it had when it was created. In-flight
|
||||
* DNS requests cannot be cancelled, but they are discarded and their results
|
||||
* will be ignored.
|
||||
*/
|
||||
destroy() {
|
||||
this.continueResolving = false;
|
||||
this.backoff.reset();
|
||||
this.backoff.stop();
|
||||
this.stopNextResolutionTimer();
|
||||
this.pendingLookupPromise = null;
|
||||
this.pendingTxtPromise = null;
|
||||
this.latestLookupResult = null;
|
||||
this.latestServiceConfig = null;
|
||||
this.latestServiceConfigError = null;
|
||||
this.returnedIpResult = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default authority for the given target. For IP targets, that is
|
||||
* the IP address. For DNS targets, it is the hostname.
|
||||
* @param target
|
||||
*/
|
||||
static getDefaultAuthority(target: GrpcUri): string {
|
||||
return target.path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the DNS resolver class by registering it as the handler for the
|
||||
* "dns:" prefix and as the default resolver.
|
||||
*/
|
||||
export function setup(): void {
|
||||
registerResolver('dns', DnsResolver);
|
||||
registerDefaultScheme('dns');
|
||||
}
|
||||
|
||||
export interface DnsUrl {
|
||||
host: string;
|
||||
port?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user