/* * 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 { Call, StatusObject } from './call-stream'; import { Channel } from './channel'; import { Status } from './constants'; import { BaseFilter, Filter, FilterFactory } from './filter'; import { Metadata } from './metadata'; const units: Array<[string, number]> = [ ['m', 1], ['S', 1000], ['M', 60 * 1000], ['H', 60 * 60 * 1000], ]; function getDeadline(deadline: number) { const now = new Date().getTime(); const timeoutMs = Math.max(deadline - now, 0); for (const [unit, factor] of units) { const amount = timeoutMs / factor; if (amount < 1e8) { return String(Math.ceil(amount)) + unit; } } throw new Error('Deadline is too far in the future'); } export class DeadlineFilter extends BaseFilter implements Filter { private timer: NodeJS.Timer | null = null; private deadline = Infinity; constructor( private readonly channel: Channel, private readonly callStream: Call ) { super(); this.retreiveDeadline(); this.runTimer(); } private retreiveDeadline() { const callDeadline = this.callStream.getDeadline(); if (callDeadline instanceof Date) { this.deadline = callDeadline.getTime(); } else { this.deadline = callDeadline; } } private runTimer() { if (this.timer) { clearTimeout(this.timer); } const now: number = new Date().getTime(); const timeout = this.deadline - now; if (timeout <= 0) { process.nextTick(() => { this.callStream.cancelWithStatus( Status.DEADLINE_EXCEEDED, 'Deadline exceeded' ); }); } else if (this.deadline !== Infinity) { this.timer = setTimeout(() => { this.callStream.cancelWithStatus( Status.DEADLINE_EXCEEDED, 'Deadline exceeded' ); }, timeout); this.timer.unref?.(); } } refresh() { this.retreiveDeadline(); this.runTimer(); } async sendMetadata(metadata: Promise) { if (this.deadline === Infinity) { return metadata; } /* The input metadata promise depends on the original channel.connect() * promise, so when it is complete that implies that the channel is * connected */ const finalMetadata = await metadata; const timeoutString = getDeadline(this.deadline); finalMetadata.set('grpc-timeout', timeoutString); return finalMetadata; } receiveTrailers(status: StatusObject) { if (this.timer) { clearTimeout(this.timer); } return status; } } export class DeadlineFilterFactory implements FilterFactory { constructor(private readonly channel: Channel) {} createFilter(callStream: Call): DeadlineFilter { return new DeadlineFilter(this.channel, callStream); } }