Merge pull request #2692 from murgatroid99/grpc-js_deadline_info

grpc-js: Add more info to deadline exceeded errors
This commit is contained in:
Michael Lumish 2024-04-01 15:25:48 -07:00 committed by GitHub
commit cc44d785c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 113 additions and 11 deletions

View File

@ -171,3 +171,7 @@ export interface Call {
getCallNumber(): number; getCallNumber(): number;
setCredentials(credentials: CallCredentials): void; setCredentials(credentials: CallCredentials): void;
} }
export interface DeadlineInfoProvider {
getDeadlineInfo(): string[];
}

View File

@ -93,3 +93,14 @@ export function deadlineToString(deadline: Deadline): string {
} }
} }
} }
/**
* Calculate the difference between two dates as a number of seconds and format
* it as a string.
* @param startDate
* @param endDate
* @returns
*/
export function formatDateDifference(startDate: Date, endDate: Date): string {
return ((endDate.getTime() - startDate.getTime()) / 1000).toFixed(3) + 's';
}

View File

@ -684,7 +684,7 @@ export class InternalChannel {
host: string, host: string,
credentials: CallCredentials, credentials: CallCredentials,
deadline: Deadline deadline: Deadline
): Call { ): LoadBalancingCall | RetryingCall {
// Create a RetryingCall if retries are enabled // Create a RetryingCall if retries are enabled
if (this.options['grpc.enable_retries'] === 0) { if (this.options['grpc.enable_retries'] === 0) {
return this.createLoadBalancingCall( return this.createLoadBalancingCall(

View File

@ -18,6 +18,7 @@
import { CallCredentials } from './call-credentials'; import { CallCredentials } from './call-credentials';
import { import {
Call, Call,
DeadlineInfoProvider,
InterceptingListener, InterceptingListener,
MessageContext, MessageContext,
StatusObject, StatusObject,
@ -25,7 +26,7 @@ import {
import { SubchannelCall } from './subchannel-call'; import { SubchannelCall } from './subchannel-call';
import { ConnectivityState } from './connectivity-state'; import { ConnectivityState } from './connectivity-state';
import { LogVerbosity, Status } from './constants'; import { LogVerbosity, Status } from './constants';
import { Deadline, getDeadlineTimeoutString } from './deadline'; import { Deadline, formatDateDifference, getDeadlineTimeoutString } from './deadline';
import { InternalChannel } from './internal-channel'; import { InternalChannel } from './internal-channel';
import { Metadata } from './metadata'; import { Metadata } from './metadata';
import { PickResultType } from './picker'; import { PickResultType } from './picker';
@ -48,7 +49,7 @@ export interface LoadBalancingCallInterceptingListener
onReceiveStatus(status: StatusObjectWithProgress): void; onReceiveStatus(status: StatusObjectWithProgress): void;
} }
export class LoadBalancingCall implements Call { export class LoadBalancingCall implements Call, DeadlineInfoProvider {
private child: SubchannelCall | null = null; private child: SubchannelCall | null = null;
private readPending = false; private readPending = false;
private pendingMessage: { context: MessageContext; message: Buffer } | null = private pendingMessage: { context: MessageContext; message: Buffer } | null =
@ -59,6 +60,8 @@ export class LoadBalancingCall implements Call {
private metadata: Metadata | null = null; private metadata: Metadata | null = null;
private listener: InterceptingListener | null = null; private listener: InterceptingListener | null = null;
private onCallEnded: ((statusCode: Status) => void) | null = null; private onCallEnded: ((statusCode: Status) => void) | null = null;
private startTime: Date;
private childStartTime: Date | null = null;
constructor( constructor(
private readonly channel: InternalChannel, private readonly channel: InternalChannel,
private readonly callConfig: CallConfig, private readonly callConfig: CallConfig,
@ -80,6 +83,26 @@ export class LoadBalancingCall implements Call {
/* Currently, call credentials are only allowed on HTTPS connections, so we /* Currently, call credentials are only allowed on HTTPS connections, so we
* can assume that the scheme is "https" */ * can assume that the scheme is "https" */
this.serviceUrl = `https://${hostname}/${serviceName}`; this.serviceUrl = `https://${hostname}/${serviceName}`;
this.startTime = new Date();
}
getDeadlineInfo(): string[] {
const deadlineInfo: string[] = [];
if (this.childStartTime) {
if (this.childStartTime > this.startTime) {
if (this.metadata?.getOptions().waitForReady) {
deadlineInfo.push('wait_for_ready');
}
deadlineInfo.push(`LB pick: ${formatDateDifference(this.startTime, this.childStartTime)}`);
}
deadlineInfo.push(...this.child!.getDeadlineInfo());
return deadlineInfo;
} else {
if (this.metadata?.getOptions().waitForReady) {
deadlineInfo.push('wait_for_ready');
}
deadlineInfo.push('Waiting for LB pick');
}
return deadlineInfo;
} }
private trace(text: string): void { private trace(text: string): void {
@ -98,7 +121,8 @@ export class LoadBalancingCall implements Call {
status.code + status.code +
' details="' + ' details="' +
status.details + status.details +
'"' '" start time=' +
this.startTime.toISOString()
); );
const finalStatus = { ...status, progress }; const finalStatus = { ...status, progress };
this.listener?.onReceiveStatus(finalStatus); this.listener?.onReceiveStatus(finalStatus);
@ -209,6 +233,7 @@ export class LoadBalancingCall implements Call {
} }
}, },
}); });
this.childStartTime = new Date();
} catch (error) { } catch (error) {
this.trace( this.trace(
'Failed to start call on picked subchannel ' + 'Failed to start call on picked subchannel ' +

View File

@ -19,6 +19,7 @@ import { CallCredentials } from './call-credentials';
import { import {
Call, Call,
CallStreamOptions, CallStreamOptions,
DeadlineInfoProvider,
InterceptingListener, InterceptingListener,
MessageContext, MessageContext,
StatusObject, StatusObject,
@ -27,6 +28,7 @@ import { LogVerbosity, Propagate, Status } from './constants';
import { import {
Deadline, Deadline,
deadlineToString, deadlineToString,
formatDateDifference,
getRelativeTimeout, getRelativeTimeout,
minDeadline, minDeadline,
} from './deadline'; } from './deadline';
@ -39,7 +41,7 @@ import { restrictControlPlaneStatusCode } from './control-plane-status';
const TRACER_NAME = 'resolving_call'; const TRACER_NAME = 'resolving_call';
export class ResolvingCall implements Call { export class ResolvingCall implements Call {
private child: Call | null = null; private child: (Call & DeadlineInfoProvider) | null = null;
private readPending = false; private readPending = false;
private pendingMessage: { context: MessageContext; message: Buffer } | null = private pendingMessage: { context: MessageContext; message: Buffer } | null =
null; null;
@ -56,6 +58,10 @@ export class ResolvingCall implements Call {
private deadlineTimer: NodeJS.Timeout = setTimeout(() => {}, 0); private deadlineTimer: NodeJS.Timeout = setTimeout(() => {}, 0);
private filterStack: FilterStack | null = null; private filterStack: FilterStack | null = null;
private deadlineStartTime: Date | null = null;
private configReceivedTime: Date | null = null;
private childStartTime: Date | null = null;
constructor( constructor(
private readonly channel: InternalChannel, private readonly channel: InternalChannel,
private readonly method: string, private readonly method: string,
@ -97,12 +103,37 @@ export class ResolvingCall implements Call {
private runDeadlineTimer() { private runDeadlineTimer() {
clearTimeout(this.deadlineTimer); clearTimeout(this.deadlineTimer);
this.deadlineStartTime = new Date();
this.trace('Deadline: ' + deadlineToString(this.deadline)); this.trace('Deadline: ' + deadlineToString(this.deadline));
const timeout = getRelativeTimeout(this.deadline); const timeout = getRelativeTimeout(this.deadline);
if (timeout !== Infinity) { if (timeout !== Infinity) {
this.trace('Deadline will be reached in ' + timeout + 'ms'); this.trace('Deadline will be reached in ' + timeout + 'ms');
const handleDeadline = () => { const handleDeadline = () => {
this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); if (!this.deadlineStartTime) {
this.cancelWithStatus(Status.DEADLINE_EXCEEDED, 'Deadline exceeded');
return;
}
const deadlineInfo: string[] = [];
const deadlineEndTime = new Date();
deadlineInfo.push(`Deadline exceeded after ${formatDateDifference(this.deadlineStartTime, deadlineEndTime)}`);
if (this.configReceivedTime) {
if (this.configReceivedTime > this.deadlineStartTime) {
deadlineInfo.push(`name resolution: ${formatDateDifference(this.deadlineStartTime, this.configReceivedTime)}`);
}
if (this.childStartTime) {
if (this.childStartTime > this.configReceivedTime) {
deadlineInfo.push(`metadata filters: ${formatDateDifference(this.configReceivedTime, this.childStartTime)}`);
}
} else {
deadlineInfo.push('waiting for metadata filters');
}
} else {
deadlineInfo.push('waiting for name resolution');
}
if (this.child) {
deadlineInfo.push(...this.child.getDeadlineInfo());
}
this.cancelWithStatus(Status.DEADLINE_EXCEEDED, deadlineInfo.join(','));
}; };
if (timeout <= 0) { if (timeout <= 0) {
process.nextTick(handleDeadline); process.nextTick(handleDeadline);
@ -176,6 +207,7 @@ export class ResolvingCall implements Call {
return; return;
} }
// configResult.type === 'SUCCESS' // configResult.type === 'SUCCESS'
this.configReceivedTime = new Date();
const config = configResult.config; const config = configResult.config;
if (config.status !== Status.OK) { if (config.status !== Status.OK) {
const { code, details } = restrictControlPlaneStatusCode( const { code, details } = restrictControlPlaneStatusCode(
@ -215,6 +247,7 @@ export class ResolvingCall implements Call {
this.deadline this.deadline
); );
this.trace('Created child [' + this.child.getCallNumber() + ']'); this.trace('Created child [' + this.child.getCallNumber() + ']');
this.childStartTime = new Date();
this.child.start(filteredMetadata, { this.child.start(filteredMetadata, {
onReceiveMetadata: metadata => { onReceiveMetadata: metadata => {
this.trace('Received metadata'); this.trace('Received metadata');

View File

@ -17,12 +17,13 @@
import { CallCredentials } from './call-credentials'; import { CallCredentials } from './call-credentials';
import { LogVerbosity, Status } from './constants'; import { LogVerbosity, Status } from './constants';
import { Deadline } from './deadline'; import { Deadline, formatDateDifference } from './deadline';
import { Metadata } from './metadata'; import { Metadata } from './metadata';
import { CallConfig } from './resolver'; import { CallConfig } from './resolver';
import * as logging from './logging'; import * as logging from './logging';
import { import {
Call, Call,
DeadlineInfoProvider,
InterceptingListener, InterceptingListener,
MessageContext, MessageContext,
StatusObject, StatusObject,
@ -121,6 +122,7 @@ interface UnderlyingCall {
state: UnderlyingCallState; state: UnderlyingCallState;
call: LoadBalancingCall; call: LoadBalancingCall;
nextMessageToSend: number; nextMessageToSend: number;
startTime: Date;
} }
/** /**
@ -170,7 +172,7 @@ interface WriteBufferEntry {
const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts'; const PREVIONS_RPC_ATTEMPTS_METADATA_KEY = 'grpc-previous-rpc-attempts';
export class RetryingCall implements Call { export class RetryingCall implements Call, DeadlineInfoProvider {
private state: RetryingCallState; private state: RetryingCallState;
private listener: InterceptingListener | null = null; private listener: InterceptingListener | null = null;
private initialMetadata: Metadata | null = null; private initialMetadata: Metadata | null = null;
@ -198,6 +200,7 @@ export class RetryingCall implements Call {
private committedCallIndex: number | null = null; private committedCallIndex: number | null = null;
private initialRetryBackoffSec = 0; private initialRetryBackoffSec = 0;
private nextRetryBackoffSec = 0; private nextRetryBackoffSec = 0;
private startTime: Date;
constructor( constructor(
private readonly channel: InternalChannel, private readonly channel: InternalChannel,
private readonly callConfig: CallConfig, private readonly callConfig: CallConfig,
@ -223,6 +226,22 @@ export class RetryingCall implements Call {
} else { } else {
this.state = 'TRANSPARENT_ONLY'; this.state = 'TRANSPARENT_ONLY';
} }
this.startTime = new Date();
}
getDeadlineInfo(): string[] {
if (this.underlyingCalls.length === 0) {
return [];
}
const deadlineInfo: string[] = [];
const latestCall = this.underlyingCalls[this.underlyingCalls.length - 1];
if (this.underlyingCalls.length > 1) {
deadlineInfo.push(`previous attempts: ${this.underlyingCalls.length - 1}`);
}
if (latestCall.startTime > this.startTime) {
deadlineInfo.push(`time to current attempt start: ${formatDateDifference(this.startTime, latestCall.startTime)}`);
}
deadlineInfo.push(...latestCall.call.getDeadlineInfo());
return deadlineInfo;
} }
getCallNumber(): number { getCallNumber(): number {
return this.callNumber; return this.callNumber;
@ -242,7 +261,8 @@ export class RetryingCall implements Call {
statusObject.code + statusObject.code +
' details="' + ' details="' +
statusObject.details + statusObject.details +
'"' '" start time=' +
this.startTime.toISOString()
); );
this.bufferTracker.freeAll(this.callNumber); this.bufferTracker.freeAll(this.callNumber);
this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length; this.writeBufferOffset = this.writeBufferOffset + this.writeBuffer.length;
@ -628,6 +648,7 @@ export class RetryingCall implements Call {
state: 'ACTIVE', state: 'ACTIVE',
call: child, call: child,
nextMessageToSend: 0, nextMessageToSend: 0,
startTime: new Date()
}); });
const previousAttempts = this.attempts - 1; const previousAttempts = this.attempts - 1;
const initialMetadata = this.initialMetadata!.clone(); const initialMetadata = this.initialMetadata!.clone();

View File

@ -15,7 +15,7 @@
* *
*/ */
import { isIP } from 'net'; import { isIP, isIPv6 } from 'net';
export interface TcpSubchannelAddress { export interface TcpSubchannelAddress {
port: number; port: number;
@ -63,7 +63,11 @@ export function subchannelAddressEqual(
export function subchannelAddressToString(address: SubchannelAddress): string { export function subchannelAddressToString(address: SubchannelAddress): string {
if (isTcpSubchannelAddress(address)) { if (isTcpSubchannelAddress(address)) {
return address.host + ':' + address.port; if (isIPv6(address.host)) {
return '[' + address.host + ']:' + address.port;
} else {
return address.host + ':' + address.port;
}
} else { } else {
return address.path; return address.path;
} }

View File

@ -70,6 +70,7 @@ export interface SubchannelCall {
startRead(): void; startRead(): void;
halfClose(): void; halfClose(): void;
getCallNumber(): number; getCallNumber(): number;
getDeadlineInfo(): string[];
} }
export interface StatusObjectWithRstCode extends StatusObject { export interface StatusObjectWithRstCode extends StatusObject {
@ -291,6 +292,9 @@ export class Http2SubchannelCall implements SubchannelCall {
this.callEventTracker.onStreamEnd(false); this.callEventTracker.onStreamEnd(false);
}); });
} }
getDeadlineInfo(): string[] {
return [`remote_addr=${this.getPeer()}`];
}
public onDisconnect() { public onDisconnect() {
this.endCall({ this.endCall({