Skip to content

Commit cc44d78

Browse files
authored
Merge pull request #2692 from murgatroid99/grpc-js_deadline_info
grpc-js: Add more info to deadline exceeded errors
2 parents 06ff525 + f4330f7 commit cc44d78

File tree

8 files changed

+113
-11
lines changed

8 files changed

+113
-11
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,7 @@ export interface Call {
171171
getCallNumber(): number;
172172
setCredentials(credentials: CallCredentials): void;
173173
}
174+
175+
export interface DeadlineInfoProvider {
176+
getDeadlineInfo(): string[];
177+
}

packages/grpc-js/src/deadline.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,14 @@ export function deadlineToString(deadline: Deadline): string {
9393
}
9494
}
9595
}
96+
97+
/**
98+
* Calculate the difference between two dates as a number of seconds and format
99+
* it as a string.
100+
* @param startDate
101+
* @param endDate
102+
* @returns
103+
*/
104+
export function formatDateDifference(startDate: Date, endDate: Date): string {
105+
return ((endDate.getTime() - startDate.getTime()) / 1000).toFixed(3) + 's';
106+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,7 @@ export class InternalChannel {
684684
host: string,
685685
credentials: CallCredentials,
686686
deadline: Deadline
687-
): Call {
687+
): LoadBalancingCall | RetryingCall {
688688
// Create a RetryingCall if retries are enabled
689689
if (this.options['grpc.enable_retries'] === 0) {
690690
return this.createLoadBalancingCall(

packages/grpc-js/src/load-balancing-call.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
import { CallCredentials } from './call-credentials';
1919
import {
2020
Call,
21+
DeadlineInfoProvider,
2122
InterceptingListener,
2223
MessageContext,
2324
StatusObject,
2425
} from './call-interface';
2526
import { SubchannelCall } from './subchannel-call';
2627
import { ConnectivityState } from './connectivity-state';
2728
import { LogVerbosity, Status } from './constants';
28-
import { Deadline, getDeadlineTimeoutString } from './deadline';
29+
import { Deadline, formatDateDifference, getDeadlineTimeoutString } from './deadline';
2930
import { InternalChannel } from './internal-channel';
3031
import { Metadata } from './metadata';
3132
import { PickResultType } from './picker';
@@ -48,7 +49,7 @@ export interface LoadBalancingCallInterceptingListener
4849
onReceiveStatus(status: StatusObjectWithProgress): void;
4950
}
5051

51-
export class LoadBalancingCall implements Call {
52+
export class LoadBalancingCall implements Call, DeadlineInfoProvider {
5253
private child: SubchannelCall | null = null;
5354
private readPending = false;
5455
private pendingMessage: { context: MessageContext; message: Buffer } | null =
@@ -59,6 +60,8 @@ export class LoadBalancingCall implements Call {
5960
private metadata: Metadata | null = null;
6061
private listener: InterceptingListener | null = null;
6162
private onCallEnded: ((statusCode: Status) => void) | null = null;
63+
private startTime: Date;
64+
private childStartTime: Date | null = null;
6265
constructor(
6366
private readonly channel: InternalChannel,
6467
private readonly callConfig: CallConfig,
@@ -80,6 +83,26 @@ export class LoadBalancingCall implements Call {
8083
/* Currently, call credentials are only allowed on HTTPS connections, so we
8184
* can assume that the scheme is "https" */
8285
this.serviceUrl = `https://${hostname}/${serviceName}`;
86+
this.startTime = new Date();
87+
}
88+
getDeadlineInfo(): string[] {
89+
const deadlineInfo: string[] = [];
90+
if (this.childStartTime) {
91+
if (this.childStartTime > this.startTime) {
92+
if (this.metadata?.getOptions().waitForReady) {
93+
deadlineInfo.push('wait_for_ready');
94+
}
95+
deadlineInfo.push(`LB pick: ${formatDateDifference(this.startTime, this.childStartTime)}`);
96+
}
97+
deadlineInfo.push(...this.child!.getDeadlineInfo());
98+
return deadlineInfo;
99+
} else {
100+
if (this.metadata?.getOptions().waitForReady) {
101+
deadlineInfo.push('wait_for_ready');
102+
}
103+
deadlineInfo.push('Waiting for LB pick');
104+
}
105+
return deadlineInfo;
83106
}
84107

85108
private trace(text: string): void {
@@ -98,7 +121,8 @@ export class LoadBalancingCall implements Call {
98121
status.code +
99122
' details="' +
100123
status.details +
101-
'"'
124+
'" start time=' +
125+
this.startTime.toISOString()
102126
);
103127
const finalStatus = { ...status, progress };
104128
this.listener?.onReceiveStatus(finalStatus);
@@ -209,6 +233,7 @@ export class LoadBalancingCall implements Call {
209233
}
210234
},
211235
});
236+
this.childStartTime = new Date();
212237
} catch (error) {
213238
this.trace(
214239
'Failed to start call on picked subchannel ' +

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { CallCredentials } from './call-credentials';
1919
import {
2020
Call,
2121
CallStreamOptions,
22+
DeadlineInfoProvider,
2223
InterceptingListener,
2324
MessageContext,
2425
StatusObject,
@@ -27,6 +28,7 @@ import { LogVerbosity, Propagate, Status } from './constants';
2728
import {
2829
Deadline,
2930
deadlineToString,
31+
formatDateDifference,
3032
getRelativeTimeout,
3133
minDeadline,
3234
} from './deadline';
@@ -39,7 +41,7 @@ import { restrictControlPlaneStatusCode } from './control-plane-status';
3941
const TRACER_NAME = 'resolving_call';
4042

4143
export class ResolvingCall implements Call {
42-
private child: Call | null = null;
44+
private child: (Call & DeadlineInfoProvider) | null = null;
4345
private readPending = false;
4446
private pendingMessage: { context: MessageContext; message: Buffer } | null =
4547
null;
@@ -56,6 +58,10 @@ export class ResolvingCall implements Call {
5658
private deadlineTimer: NodeJS.Timeout = setTimeout(() => {}, 0);
5759
private filterStack: FilterStack | null = null;
5860

61+
private deadlineStartTime: Date | null = null;
62+
private configReceivedTime: Date | null = null;
63+
private childStartTime: Date | null = null;
64+
5965
constructor(
6066
private readonly channel: InternalChannel,
6167
private readonly method: string,
@@ -97,12 +103,37 @@ export class ResolvingCall implements Call {
97103

98104
private runDeadlineTimer() {
99105
clearTimeout(this.deadlineTimer);
106+
this.deadlineStartTime = new Date();
100107
this.trace('Deadline: ' + deadlineToString(this.deadline));
101108
const timeout = getRelativeTimeout(this.deadline);
102109
if (timeout !== Infinity) {
103110
this.trace('Deadline will be reached in ' + timeout + 'ms');
104111
const handleDeadline = () => {
105-
this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded');
112+
if (!this.deadlineStartTime) {
113+
this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded');
114+
return;
115+
}
116+
const deadlineInfo: string[] = [];
117+
const deadlineEndTime = new Date();
118+
deadlineInfo.push(`Deadline exceeded after ${formatDateDifference(this.deadlineStartTime, deadlineEndTime)}`);
119+
if (this.configReceivedTime) {
120+
if (this.configReceivedTime > this.deadlineStartTime) {
121+
deadlineInfo.push(`name resolution: ${formatDateDifference(this.deadlineStartTime, this.configReceivedTime)}`);
122+
}
123+
if (this.childStartTime) {
124+
if (this.childStartTime > this.configReceivedTime) {
125+
deadlineInfo.push(`metadata filters: ${formatDateDifference(this.configReceivedTime, this.childStartTime)}`);
126+
}
127+
} else {
128+
deadlineInfo.push('waiting for metadata filters');
129+
}
130+
} else {
131+
deadlineInfo.push('waiting for name resolution');
132+
}
133+
if (this.child) {
134+
deadlineInfo.push(...this.child.getDeadlineInfo());
135+
}
136+
this.cancelWithStatus(Status.DEADLINE_EXCEEDED, deadlineInfo.join(','));
106137
};
107138
if (timeout <= 0) {
108139
process.nextTick(handleDeadline);
@@ -176,6 +207,7 @@ export class ResolvingCall implements Call {
176207
return;
177208
}
178209
// configResult.type === 'SUCCESS'
210+
this.configReceivedTime = new Date();
179211
const config = configResult.config;
180212
if (config.status !== Status.OK) {
181213
const { code, details } = restrictControlPlaneStatusCode(
@@ -215,6 +247,7 @@ export class ResolvingCall implements Call {
215247
this.deadline
216248
);
217249
this.trace('Created child [' + this.child.getCallNumber() + ']');
250+
this.childStartTime = new Date();
218251
this.child.start(filteredMetadata, {
219252
onReceiveMetadata: metadata => {
220253
this.trace('Received metadata');

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717

1818
import { CallCredentials } from './call-credentials';
1919
import { LogVerbosity, Status } from './constants';
20-
import { Deadline } from './deadline';
20+
import { Deadline, formatDateDifference } from './deadline';
2121
import { Metadata } from './metadata';
2222
import { CallConfig } from './resolver';
2323
import * as logging from './logging';
2424
import {
2525
Call,
26+
DeadlineInfoProvider,
2627
InterceptingListener,
2728
MessageContext,
2829
StatusObject,
@@ -121,6 +122,7 @@ interface UnderlyingCall {
121122
state: UnderlyingCallState;
122123
call: LoadBalancingCall;
123124
nextMessageToSend: number;
125+
startTime: Date;
124126
}
125127

126128
/**
@@ -170,7 +172,7 @@ interface WriteBufferEntry {
170172

171173
const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts';
172174

173-
export class RetryingCall implements Call {
175+
export class RetryingCall implements Call, DeadlineInfoProvider {
174176
private state: RetryingCallState;
175177
private listener: InterceptingListener | null = null;
176178
private initialMetadata: Metadata | null = null;
@@ -198,6 +200,7 @@ export class RetryingCall implements Call {
198200
private committedCallIndex: number | null = null;
199201
private initialRetryBackoffSec = 0;
200202
private nextRetryBackoffSec = 0;
203+
private startTime: Date;
201204
constructor(
202205
private readonly channel: InternalChannel,
203206
private readonly callConfig: CallConfig,
@@ -223,6 +226,22 @@ export class RetryingCall implements Call {
223226
} else {
224227
this.state = 'TRANSPARENT_ONLY';
225228
}
229+
this.startTime = new Date();
230+
}
231+
getDeadlineInfo(): string[] {
232+
if (this.underlyingCalls.length === 0) {
233+
return [];
234+
}
235+
const deadlineInfo: string[] = [];
236+
const latestCall = this.underlyingCalls[this.underlyingCalls.length - 1];
237+
if (this.underlyingCalls.length > 1) {
238+
deadlineInfo.push(`previous attempts: ${this.underlyingCalls.length - 1}`);
239+
}
240+
if (latestCall.startTime > this.startTime) {
241+
deadlineInfo.push(`time to current attempt start: ${formatDateDifference(this.startTime, latestCall.startTime)}`);
242+
}
243+
deadlineInfo.push(...latestCall.call.getDeadlineInfo());
244+
return deadlineInfo;
226245
}
227246
getCallNumber(): number {
228247
return this.callNumber;
@@ -242,7 +261,8 @@ export class RetryingCall implements Call {
242261
statusObject.code +
243262
' details="' +
244263
statusObject.details +
245-
'"'
264+
'" start time=' +
265+
this.startTime.toISOString()
246266
);
247267
this.bufferTracker.freeAll(this.callNumber);
248268
this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length;
@@ -628,6 +648,7 @@ export class RetryingCall implements Call {
628648
state: 'ACTIVE',
629649
call: child,
630650
nextMessageToSend: 0,
651+
startTime: new Date()
631652
});
632653
const previousAttempts = this.attempts - 1;
633654
const initialMetadata = this.initialMetadata!.clone();

packages/grpc-js/src/subchannel-address.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*
1616
*/
1717

18-
import { isIP } from 'net';
18+
import { isIP, isIPv6 } from 'net';
1919

2020
export interface TcpSubchannelAddress {
2121
port: number;
@@ -63,7 +63,11 @@ export function subchannelAddressEqual(
6363

6464
export function subchannelAddressToString(address: SubchannelAddress): string {
6565
if (isTcpSubchannelAddress(address)) {
66-
return address.host + ':' + address.port;
66+
if (isIPv6(address.host)) {
67+
return '[' + address.host + ']:' + address.port;
68+
} else {
69+
return address.host + ':' + address.port;
70+
}
6771
} else {
6872
return address.path;
6973
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface SubchannelCall {
7070
startRead(): void;
7171
halfClose(): void;
7272
getCallNumber(): number;
73+
getDeadlineInfo(): string[];
7374
}
7475

7576
export interface StatusObjectWithRstCode extends StatusObject {
@@ -291,6 +292,9 @@ export class Http2SubchannelCall implements SubchannelCall {
291292
this.callEventTracker.onStreamEnd(false);
292293
});
293294
}
295+
getDeadlineInfo(): string[] {
296+
return [`remote_addr=${this.getPeer()}`];
297+
}
294298

295299
public onDisconnect() {
296300
this.endCall({

0 commit comments

Comments
 (0)