UNPKG

3.36 kBPlain TextView Raw
1/*
2 * Copyright 2019 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import { Call, StatusObject } from './call-stream';
19import { Channel } from './channel';
20import { Status } from './constants';
21import { BaseFilter, Filter, FilterFactory } from './filter';
22import { Metadata } from './metadata';
23
24const units: Array<[string, number]> = [
25 ['m', 1],
26 ['S', 1000],
27 ['M', 60 * 1000],
28 ['H', 60 * 60 * 1000],
29];
30
31function getDeadline(deadline: number) {
32 const now = new Date().getTime();
33 const timeoutMs = Math.max(deadline - now, 0);
34 for (const [unit, factor] of units) {
35 const amount = timeoutMs / factor;
36 if (amount < 1e8) {
37 return String(Math.ceil(amount)) + unit;
38 }
39 }
40 throw new Error('Deadline is too far in the future');
41}
42
43export class DeadlineFilter extends BaseFilter implements Filter {
44 private timer: NodeJS.Timer | null = null;
45 private deadline = Infinity;
46 constructor(
47 private readonly channel: Channel,
48 private readonly callStream: Call
49 ) {
50 super();
51 this.retreiveDeadline();
52 this.runTimer();
53 }
54
55 private retreiveDeadline() {
56 const callDeadline = this.callStream.getDeadline();
57 if (callDeadline instanceof Date) {
58 this.deadline = callDeadline.getTime();
59 } else {
60 this.deadline = callDeadline;
61 }
62 }
63
64 private runTimer() {
65 if (this.timer) {
66 clearTimeout(this.timer);
67 }
68 const now: number = new Date().getTime();
69 const timeout = this.deadline - now;
70 if (timeout <= 0) {
71 process.nextTick(() => {
72 this.callStream.cancelWithStatus(
73 Status.DEADLINE_EXCEEDED,
74 'Deadline exceeded'
75 );
76 });
77 } else if (this.deadline !== Infinity) {
78 this.timer = setTimeout(() => {
79 this.callStream.cancelWithStatus(
80 Status.DEADLINE_EXCEEDED,
81 'Deadline exceeded'
82 );
83 }, timeout);
84 this.timer.unref?.();
85 }
86 }
87
88 refresh() {
89 this.retreiveDeadline();
90 this.runTimer();
91 }
92
93 async sendMetadata(metadata: Promise<Metadata>) {
94 if (this.deadline === Infinity) {
95 return metadata;
96 }
97 /* The input metadata promise depends on the original channel.connect()
98 * promise, so when it is complete that implies that the channel is
99 * connected */
100 const finalMetadata = await metadata;
101 const timeoutString = getDeadline(this.deadline);
102 finalMetadata.set('grpc-timeout', timeoutString);
103 return finalMetadata;
104 }
105
106 receiveTrailers(status: StatusObject) {
107 if (this.timer) {
108 clearTimeout(this.timer);
109 }
110 return status;
111 }
112}
113
114export class DeadlineFilterFactory implements FilterFactory<DeadlineFilter> {
115 constructor(private readonly channel: Channel) {}
116
117 createFilter(callStream: Call): DeadlineFilter {
118 return new DeadlineFilter(this.channel, callStream);
119 }
120}