Skip to content

Commit 663fe77

Browse files
authored
Merge pull request #1785 from murgatroid99/grpc-js_service_config_timeout
grpc-js: Apply timeouts from service configs
2 parents 1e9bf30 + e3106b9 commit 663fe77

File tree

6 files changed

+89
-16
lines changed

6 files changed

+89
-16
lines changed

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

+23-7
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,17 @@ function getSystemErrorName(errno: number): string {
7070

7171
export type Deadline = Date | number;
7272

73+
function getMinDeadline(deadlineList: Deadline[]): Deadline {
74+
let minValue: number = Infinity;
75+
for (const deadline of deadlineList) {
76+
const deadlineMsecs = deadline instanceof Date ? deadline.getTime() : deadline;
77+
if (deadlineMsecs < minValue) {
78+
minValue = deadlineMsecs;
79+
}
80+
}
81+
return minValue;
82+
}
83+
7384
export interface CallStreamOptions {
7485
deadline: Deadline;
7586
flags: number;
@@ -235,6 +246,8 @@ export class Http2CallStream implements Call {
235246

236247
private internalError: SystemError | null = null;
237248

249+
private configDeadline: Deadline = Infinity;
250+
238251
constructor(
239252
private readonly methodName: string,
240253
private readonly channel: ChannelImplementation,
@@ -675,15 +688,14 @@ export class Http2CallStream implements Call {
675688
}
676689

677690
getDeadline(): Deadline {
691+
const deadlineList = [this.options.deadline];
678692
if (this.options.parentCall && this.options.flags & Propagate.DEADLINE) {
679-
const parentDeadline = this.options.parentCall.getDeadline();
680-
const selfDeadline = this.options.deadline;
681-
const parentDeadlineMsecs = parentDeadline instanceof Date ? parentDeadline.getTime() : parentDeadline;
682-
const selfDeadlineMsecs = selfDeadline instanceof Date ? selfDeadline.getTime() : selfDeadline;
683-
return Math.min(parentDeadlineMsecs, selfDeadlineMsecs);
684-
} else {
685-
return this.options.deadline;
693+
deadlineList.push(this.options.parentCall.getDeadline());
694+
}
695+
if (this.configDeadline) {
696+
deadlineList.push(this.configDeadline);
686697
}
698+
return getMinDeadline(deadlineList);
687699
}
688700

689701
getCredentials(): CallCredentials {
@@ -710,6 +722,10 @@ export class Http2CallStream implements Call {
710722
return this.options.host;
711723
}
712724

725+
setConfigDeadline(configDeadline: Deadline) {
726+
this.configDeadline = configDeadline;
727+
}
728+
713729
startRead() {
714730
/* If the stream has ended with an error, we should not emit any more
715731
* messages and we should communicate that the stream has ended */

packages/grpc-js/src/channel.ts

+13
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,11 @@ export class ChannelImplementation implements Channel {
509509
}
510510

511511
private tryGetConfig(stream: Http2CallStream, metadata: Metadata) {
512+
if (stream.getStatus() !== null) {
513+
/* If the stream has a status, it has already finished and we don't need
514+
* to take any more actions on it. */
515+
return;
516+
}
512517
if (this.configSelector === null) {
513518
/* This branch will only be taken at the beginning of the channel's life,
514519
* before the resolver ever returns a result. So, the
@@ -523,6 +528,14 @@ export class ChannelImplementation implements Channel {
523528
} else {
524529
const callConfig = this.configSelector(stream.getMethod(), metadata);
525530
if (callConfig.status === Status.OK) {
531+
if (callConfig.methodConfig.timeout) {
532+
const deadline = new Date();
533+
deadline.setSeconds(deadline.getSeconds() + callConfig.methodConfig.timeout.seconds);
534+
deadline.setMilliseconds(deadline.getMilliseconds() + callConfig.methodConfig.timeout.nanos / 1_000_000);
535+
stream.setConfigDeadline(deadline);
536+
// Refreshing the filters makes the deadline filter pick up the new deadline
537+
stream.filterStack.refresh();
538+
}
526539
this.tryPick(stream, metadata, callConfig);
527540
} else {
528541
stream.cancelWithStatus(callConfig.status, "Failed to route call to method " + stream.getMethod());

packages/grpc-js/src/deadline-filter.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -42,30 +42,41 @@ function getDeadline(deadline: number) {
4242

4343
export class DeadlineFilter extends BaseFilter implements Filter {
4444
private timer: NodeJS.Timer | null = null;
45-
private deadline: number;
45+
private deadline: number = Infinity;
4646
constructor(
4747
private readonly channel: Channel,
4848
private readonly callStream: Call
4949
) {
5050
super();
51-
const callDeadline = callStream.getDeadline();
51+
this.retreiveDeadline();
52+
this.runTimer();
53+
}
54+
55+
private retreiveDeadline() {
56+
const callDeadline = this.callStream.getDeadline();
5257
if (callDeadline instanceof Date) {
5358
this.deadline = callDeadline.getTime();
5459
} else {
5560
this.deadline = callDeadline;
5661
}
62+
}
63+
64+
private runTimer() {
65+
if (this.timer) {
66+
clearTimeout(this.timer);
67+
}
5768
const now: number = new Date().getTime();
5869
let timeout = this.deadline - now;
5970
if (timeout <= 0) {
6071
process.nextTick(() => {
61-
callStream.cancelWithStatus(
72+
this.callStream.cancelWithStatus(
6273
Status.DEADLINE_EXCEEDED,
6374
'Deadline exceeded'
6475
);
6576
});
6677
} else if (this.deadline !== Infinity) {
6778
this.timer = setTimeout(() => {
68-
callStream.cancelWithStatus(
79+
this.callStream.cancelWithStatus(
6980
Status.DEADLINE_EXCEEDED,
7081
'Deadline exceeded'
7182
);
@@ -74,6 +85,11 @@ export class DeadlineFilter extends BaseFilter implements Filter {
7485
}
7586
}
7687

88+
refresh() {
89+
this.retreiveDeadline();
90+
this.runTimer();
91+
}
92+
7793
async sendMetadata(metadata: Promise<Metadata>) {
7894
if (this.deadline === Infinity) {
7995
return metadata;

packages/grpc-js/src/filter-stack.ts

+6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export class FilterStack implements Filter {
7171

7272
return result;
7373
}
74+
75+
refresh(): void {
76+
for (const filter of this.filters) {
77+
filter.refresh();
78+
}
79+
}
7480
}
7581

7682
export class FilterStackFactory implements FilterFactory<FilterStack> {

packages/grpc-js/src/filter.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface Filter {
3232
receiveMessage(message: Promise<Buffer>): Promise<Buffer>;
3333

3434
receiveTrailers(status: StatusObject): StatusObject;
35+
36+
refresh(): void;
3537
}
3638

3739
export abstract class BaseFilter implements Filter {
@@ -54,6 +56,9 @@ export abstract class BaseFilter implements Filter {
5456
receiveTrailers(status: StatusObject): StatusObject {
5557
return status;
5658
}
59+
60+
refresh(): void {
61+
}
5762
}
5863

5964
export interface FilterFactory<T extends Filter> {

packages/grpc-js/src/service-config.ts

+22-5
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ export interface MethodConfigName {
3434
method?: string;
3535
}
3636

37+
export interface Duration {
38+
seconds: number;
39+
nanos: number;
40+
}
41+
3742
export interface MethodConfig {
3843
name: MethodConfigName[];
3944
waitForReady?: boolean;
40-
timeout?: string;
45+
timeout?: Duration;
4146
maxRequestBytes?: number;
4247
maxResponseBytes?: number;
4348
}
@@ -101,13 +106,25 @@ function validateMethodConfig(obj: any): MethodConfig {
101106
result.waitForReady = obj.waitForReady;
102107
}
103108
if ('timeout' in obj) {
104-
if (
105-
!(typeof obj.timeout === 'string') ||
106-
!TIMEOUT_REGEX.test(obj.timeout)
109+
if (typeof obj.timeout === 'object') {
110+
if (!('seconds' in obj.timeout) || !(typeof obj.timeout.seconds === 'number')) {
111+
throw new Error('Invalid method config: invalid timeout.seconds');
112+
}
113+
if (!('nanos' in obj.timeout) || !(typeof obj.timeout.nanos === 'number')) {
114+
throw new Error('Invalid method config: invalid timeout.nanos');
115+
}
116+
result.timeout = obj.timeout;
117+
} else if (
118+
(typeof obj.timeout === 'string') && TIMEOUT_REGEX.test(obj.timeout)
107119
) {
120+
const timeoutParts = obj.timeout.substring(0, obj.timeout.length - 1).split('.');
121+
result.timeout = {
122+
seconds: timeoutParts[0] | 0,
123+
nanos: (timeoutParts[1] ?? 0) | 0
124+
}
125+
} else {
108126
throw new Error('Invalid method config: invalid timeout');
109127
}
110-
result.timeout = obj.timeout;
111128
}
112129
if ('maxRequestBytes' in obj) {
113130
if (typeof obj.maxRequestBytes !== 'number') {

0 commit comments

Comments
 (0)