Skip to content

Commit 422e8cb

Browse files
authored
Merge pull request #2078 from grpc/@grpc/[email protected]
Upmerge 1.6.x into master
2 parents a6f3df7 + 4e457b5 commit 422e8cb

File tree

9 files changed

+129
-23
lines changed

9 files changed

+129
-23
lines changed

packages/grpc-js-xds/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ const client = new MyServiceClient('xds:///example.com:123');
2727
- [xDS Timeouts](https://github.com/grpc/proposal/blob/master/A31-xds-timeout-support-and-config-selector.md)
2828
- [xDS Circuit Breaking](https://github.com/grpc/proposal/blob/master/A32-xds-circuit-breaking.md)
2929
- [xDS Client-Side Fault Injection](https://github.com/grpc/proposal/blob/master/A33-Fault-Injection.md)
30-
- [Client Status Discovery Service](https://github.com/grpc/proposal/blob/master/A40-csds-support.md)
30+
- [Client Status Discovery Service](https://github.com/grpc/proposal/blob/master/A40-csds-support.md)
31+
- [Outlier Detection](https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md) (experimental, disabled by default, enabled by setting the environment variable `GRPC_EXPERIMENTAL_ENABLE_OUTLIER_DETECTION=true`)

packages/grpc-js-xds/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grpc/grpc-js-xds",
3-
"version": "1.5.2",
3+
"version": "1.6.1",
44
"description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.",
55
"main": "build/src/index.js",
66
"scripts": {
@@ -47,7 +47,7 @@
4747
"re2-wasm": "^1.0.1"
4848
},
4949
"peerDependencies": {
50-
"@grpc/grpc-js": "~1.5.0"
50+
"@grpc/grpc-js": "~1.6.0"
5151
},
5252
"engines": {
5353
"node": ">=10.10.0"

packages/grpc-js-xds/src/xds-client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,6 @@ export class XdsClient {
688688
ack(serviceKind: AdsServiceKind) {
689689
/* An ack is the best indication of a successful interaction between the
690690
* client and the server, so we can reset the backoff timer here. */
691-
this.adsBackoff.stop();
692691
this.adsBackoff.reset();
693692

694693
this.updateNames(serviceKind);

packages/grpc-js/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Many channel arguments supported in `grpc` are not supported in `@grpc/grpc-js`.
5858
- `grpc.enable_http_proxy`
5959
- `grpc.default_compression_algorithm`
6060
- `grpc.enable_channelz`
61+
- `grpc.dns_min_time_between_resolutions_ms`
6162
- `grpc-node.max_session_memory`
6263
- `channelOverride`
6364
- `channelFactoryOverride`

packages/grpc-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grpc/grpc-js",
3-
"version": "1.5.10",
3+
"version": "1.6.2",
44
"description": "gRPC Library for Node - pure JS implementation",
55
"homepage": "https://grpc.io/",
66
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",

packages/grpc-js/src/backoff-timeout.ts

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,47 @@ export interface BackoffOptions {
3737
}
3838

3939
export class BackoffTimeout {
40-
private initialDelay: number = INITIAL_BACKOFF_MS;
41-
private multiplier: number = BACKOFF_MULTIPLIER;
42-
private maxDelay: number = MAX_BACKOFF_MS;
43-
private jitter: number = BACKOFF_JITTER;
40+
/**
41+
* The delay time at the start, and after each reset.
42+
*/
43+
private readonly initialDelay: number = INITIAL_BACKOFF_MS;
44+
/**
45+
* The exponential backoff multiplier.
46+
*/
47+
private readonly multiplier: number = BACKOFF_MULTIPLIER;
48+
/**
49+
* The maximum delay time
50+
*/
51+
private readonly maxDelay: number = MAX_BACKOFF_MS;
52+
/**
53+
* The maximum fraction by which the delay time can randomly vary after
54+
* applying the multiplier.
55+
*/
56+
private readonly jitter: number = BACKOFF_JITTER;
57+
/**
58+
* The delay time for the next time the timer runs.
59+
*/
4460
private nextDelay: number;
61+
/**
62+
* The handle of the underlying timer. If running is false, this value refers
63+
* to an object representing a timer that has ended, but it can still be
64+
* interacted with without error.
65+
*/
4566
private timerId: NodeJS.Timer;
67+
/**
68+
* Indicates whether the timer is currently running.
69+
*/
4670
private running = false;
71+
/**
72+
* Indicates whether the timer should keep the Node process running if no
73+
* other async operation is doing so.
74+
*/
4775
private hasRef = true;
76+
/**
77+
* The time that the currently running timer was started. Only valid if
78+
* running is true.
79+
*/
80+
private startTime: Date = new Date();
4881

4982
constructor(private callback: () => void, options?: BackoffOptions) {
5083
if (options) {
@@ -66,18 +99,23 @@ export class BackoffTimeout {
6699
clearTimeout(this.timerId);
67100
}
68101

69-
/**
70-
* Call the callback after the current amount of delay time
71-
*/
72-
runOnce() {
73-
this.running = true;
102+
private runTimer(delay: number) {
74103
this.timerId = setTimeout(() => {
75104
this.callback();
76105
this.running = false;
77-
}, this.nextDelay);
106+
}, delay);
78107
if (!this.hasRef) {
79108
this.timerId.unref?.();
80109
}
110+
}
111+
112+
/**
113+
* Call the callback after the current amount of delay time
114+
*/
115+
runOnce() {
116+
this.running = true;
117+
this.startTime = new Date();
118+
this.runTimer(this.nextDelay);
81119
const nextBackoff = Math.min(
82120
this.nextDelay * this.multiplier,
83121
this.maxDelay
@@ -97,21 +135,44 @@ export class BackoffTimeout {
97135
}
98136

99137
/**
100-
* Reset the delay time to its initial value.
138+
* Reset the delay time to its initial value. If the timer is still running,
139+
* retroactively apply that reset to the current timer.
101140
*/
102141
reset() {
103142
this.nextDelay = this.initialDelay;
143+
if (this.running) {
144+
const now = new Date();
145+
const newEndTime = this.startTime;
146+
newEndTime.setMilliseconds(newEndTime.getMilliseconds() + this.nextDelay);
147+
clearTimeout(this.timerId);
148+
if (now < newEndTime) {
149+
this.runTimer(newEndTime.getTime() - now.getTime());
150+
} else {
151+
this.running = false;
152+
}
153+
}
104154
}
105155

156+
/**
157+
* Check whether the timer is currently running.
158+
*/
106159
isRunning() {
107160
return this.running;
108161
}
109162

163+
/**
164+
* Set that while the timer is running, it should keep the Node process
165+
* running.
166+
*/
110167
ref() {
111168
this.hasRef = true;
112169
this.timerId.ref?.();
113170
}
114171

172+
/**
173+
* Set that while the timer is running, it should not keep the Node process
174+
* running.
175+
*/
115176
unref() {
116177
this.hasRef = false;
117178
this.timerId.unref?.();

packages/grpc-js/src/call-stream.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,16 @@ export class Http2CallStream implements Call {
839839
message,
840840
flags: context.flags,
841841
};
842-
const cb: WriteCallback = context.callback ?? (() => {});
842+
const cb: WriteCallback = (error?: Error | null) => {
843+
let code: Status = Status.UNAVAILABLE;
844+
if ((error as NodeJS.ErrnoException)?.code === 'ERR_STREAM_WRITE_AFTER_END') {
845+
code = Status.INTERNAL;
846+
}
847+
if (error) {
848+
this.cancelWithStatus(code, `Write error: ${error.message}`);
849+
}
850+
context.callback?.();
851+
};
843852
this.isWriteFilterPending = true;
844853
this.filterStack.sendMessage(Promise.resolve(writeObj)).then((message) => {
845854
this.isWriteFilterPending = false;

packages/grpc-js/src/channel-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface ChannelOptions {
4343
'grpc.http_connect_creds'?: string;
4444
'grpc.default_compression_algorithm'?: CompressionAlgorithms;
4545
'grpc.enable_channelz'?: number;
46+
'grpc.dns_min_time_between_resolutions_ms'?: number;
4647
'grpc-node.max_session_memory'?: number;
4748
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4849
[key: string]: any;
@@ -69,6 +70,7 @@ export const recognizedOptions = {
6970
'grpc.max_receive_message_length': true,
7071
'grpc.enable_http_proxy': true,
7172
'grpc.enable_channelz': true,
73+
'grpc.dns_min_time_between_resolutions_ms': true,
7274
'grpc-node.max_session_memory': true,
7375
};
7476

packages/grpc-js/src/resolver-dns.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ function trace(text: string): void {
4545
*/
4646
const DEFAULT_PORT = 443;
4747

48+
const DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS = 30_000;
49+
4850
const resolveTxtPromise = util.promisify(dns.resolveTxt);
4951
const dnsLookupPromise = util.promisify(dns.lookup);
5052

@@ -79,6 +81,12 @@ class DnsResolver implements Resolver {
7981
private readonly ipResult: SubchannelAddress[] | null;
8082
private readonly dnsHostname: string | null;
8183
private readonly port: number | null;
84+
/**
85+
* Minimum time between resolutions, measured as the time between starting
86+
* successive resolution requests. Only applies to successful resolutions.
87+
* Failures are handled by the backoff timer.
88+
*/
89+
private readonly minTimeBetweenResolutionsMs: number;
8290
private pendingLookupPromise: Promise<dns.LookupAddress[]> | null = null;
8391
private pendingTxtPromise: Promise<string[][]> | null = null;
8492
private latestLookupResult: TcpSubchannelAddress[] | null = null;
@@ -88,6 +96,8 @@ class DnsResolver implements Resolver {
8896
private defaultResolutionError: StatusObject;
8997
private backoff: BackoffTimeout;
9098
private continueResolving = false;
99+
private nextResolutionTimer: NodeJS.Timer;
100+
private isNextResolutionTimerRunning = false;
91101
constructor(
92102
private target: GrpcUri,
93103
private listener: ResolverListener,
@@ -134,6 +144,10 @@ class DnsResolver implements Resolver {
134144
}
135145
}, backoffOptions);
136146
this.backoff.unref();
147+
148+
this.minTimeBetweenResolutionsMs = channelOptions['grpc.dns_min_time_between_resolutions_ms'] ?? DEFAULT_MIN_TIME_BETWEEN_RESOLUTIONS_MS;
149+
this.nextResolutionTimer = setTimeout(() => {}, 0);
150+
clearTimeout(this.nextResolutionTimer);
137151
}
138152

139153
/**
@@ -183,6 +197,7 @@ class DnsResolver implements Resolver {
183197
(addressList) => {
184198
this.pendingLookupPromise = null;
185199
this.backoff.reset();
200+
this.backoff.stop();
186201
const ip4Addresses: dns.LookupAddress[] = addressList.filter(
187202
(addr) => addr.family === 4
188203
);
@@ -229,6 +244,7 @@ class DnsResolver implements Resolver {
229244
(err as Error).message
230245
);
231246
this.pendingLookupPromise = null;
247+
this.stopNextResolutionTimer();
232248
this.listener.onError(this.defaultResolutionError);
233249
}
234250
);
@@ -282,17 +298,34 @@ class DnsResolver implements Resolver {
282298
}
283299
}
284300

301+
private startNextResolutionTimer() {
302+
this.nextResolutionTimer = setTimeout(() => {
303+
this.stopNextResolutionTimer();
304+
if (this.continueResolving) {
305+
this.startResolutionWithBackoff();
306+
}
307+
}, this.minTimeBetweenResolutionsMs).unref?.();
308+
this.isNextResolutionTimerRunning = true;
309+
}
310+
311+
private stopNextResolutionTimer() {
312+
clearTimeout(this.nextResolutionTimer);
313+
this.isNextResolutionTimerRunning = false;
314+
}
315+
285316
private startResolutionWithBackoff() {
286317
this.startResolution();
287318
this.backoff.runOnce();
319+
this.startNextResolutionTimer();
288320
}
289321

290322
updateResolution() {
291323
/* If there is a pending lookup, just let it finish. Otherwise, if the
292-
* backoff timer is running, do another lookup when it ends, and if not,
293-
* do another lookup immeidately. */
324+
* nextResolutionTimer or backoff timer is running, set the
325+
* continueResolving flag to resolve when whichever of those timers
326+
* fires. Otherwise, start resolving immediately. */
294327
if (this.pendingLookupPromise === null) {
295-
if (this.backoff.isRunning()) {
328+
if (this.isNextResolutionTimerRunning || this.backoff.isRunning()) {
296329
this.continueResolving = true;
297330
} else {
298331
this.startResolutionWithBackoff();
@@ -301,9 +334,9 @@ class DnsResolver implements Resolver {
301334
}
302335

303336
destroy() {
304-
/* Do nothing. There is not a practical way to cancel in-flight DNS
305-
* requests, and after this function is called we can expect that
306-
* updateResolution will not be called again. */
337+
this.continueResolving = false;
338+
this.backoff.stop();
339+
this.stopNextResolutionTimer();
307340
}
308341

309342
/**

0 commit comments

Comments
 (0)