From 8a503031f3db9d83f45a887b78db15d14fd275c2 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Fri, 1 Apr 2022 10:01:47 -0700 Subject: [PATCH] grpc-js: Add support for grpc.dns_min_time_between_resolutions_ms channel arg --- packages/grpc-js/README.md | 1 + packages/grpc-js/package.json | 2 +- packages/grpc-js/src/channel-options.ts | 2 ++ packages/grpc-js/src/resolver-dns.ts | 45 +++++++++++++++++++++---- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/grpc-js/README.md b/packages/grpc-js/README.md index 71ec937ed..11a8ca9aa 100644 --- a/packages/grpc-js/README.md +++ b/packages/grpc-js/README.md @@ -58,6 +58,7 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`. - `grpc.enable_http_proxy` - `grpc.default_compression_algorithm` - `grpc.enable_channelz` + - `grpc.dns_min_time_between_resolutions_ms` - `grpc-node.max_session_memory` - `channelOverride` - `channelFactoryOverride` diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 928260be6..ecc3e30cd 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.6.0", + "version": "1.6.1", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/channel-options.ts b/packages/grpc-js/src/channel-options.ts index 688317227..b7fc92fa9 100644 --- a/packages/grpc-js/src/channel-options.ts +++ b/packages/grpc-js/src/channel-options.ts @@ -43,6 +43,7 @@ export interface ChannelOptions { 'grpc.http_connect_creds'?: string; 'grpc.default_compression_algorithm'?: CompressionAlgorithms; 'grpc.enable_channelz'?: number; + 'grpc.dns_min_time_between_resolutions_ms'?: number; 'grpc-node.max_session_memory'?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; @@ -69,6 +70,7 @@ export const recognizedOptions = { 'grpc.max_receive_message_length': true, 'grpc.enable_http_proxy': true, 'grpc.enable_channelz': true, + 'grpc.dns_min_time_between_resolutions_ms': true, 'grpc-node.max_session_memory': true, }; diff --git a/packages/grpc-js/src/resolver-dns.ts b/packages/grpc-js/src/resolver-dns.ts index 8ad24ed05..c4cb64a74 100644 --- a/packages/grpc-js/src/resolver-dns.ts +++ b/packages/grpc-js/src/resolver-dns.ts @@ -45,6 +45,8 @@ function trace(text: string): void { */ 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); @@ -79,6 +81,12 @@ 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 | null = null; private pendingTxtPromise: Promise | null = null; private latestLookupResult: TcpSubchannelAddress[] | null = null; @@ -88,6 +96,8 @@ class DnsResolver implements Resolver { private defaultResolutionError: StatusObject; private backoff: BackoffTimeout; private continueResolving = false; + private nextResolutionTimer: NodeJS.Timer; + private isNextResolutionTimerRunning = false; constructor( private target: GrpcUri, private listener: ResolverListener, @@ -134,6 +144,10 @@ class DnsResolver implements Resolver { } }, 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); } /** @@ -183,6 +197,7 @@ class DnsResolver implements Resolver { (addressList) => { this.pendingLookupPromise = null; this.backoff.reset(); + this.backoff.stop(); const ip4Addresses: dns.LookupAddress[] = addressList.filter( (addr) => addr.family === 4 ); @@ -229,6 +244,7 @@ class DnsResolver implements Resolver { (err as Error).message ); this.pendingLookupPromise = null; + this.stopNextResolutionTimer(); this.listener.onError(this.defaultResolutionError); } ); @@ -282,17 +298,34 @@ class DnsResolver implements Resolver { } } + private startNextResolutionTimer() { + 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() { this.startResolution(); this.backoff.runOnce(); + this.startNextResolutionTimer(); } updateResolution() { /* If there is a pending lookup, just let it finish. Otherwise, if the - * backoff timer is running, do another lookup when it ends, and if not, - * do another lookup immeidately. */ + * 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.backoff.isRunning()) { + if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) { this.continueResolving = true; } else { this.startResolutionWithBackoff(); @@ -301,9 +334,9 @@ class DnsResolver implements Resolver { } destroy() { - /* Do nothing. There is not a practical way to cancel in-flight DNS - * requests, and after this function is called we can expect that - * updateResolution will not be called again. */ + this.continueResolving = false; + this.backoff.stop(); + this.stopNextResolutionTimer(); } /**