Skip to content

Commit 3a9f4d2

Browse files
committed
grpc-js: Propagate connectivity error information to request errors
1 parent 065ac2f commit 3a9f4d2

File tree

7 files changed

+47
-24
lines changed

7 files changed

+47
-24
lines changed

packages/grpc-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grpc/grpc-js",
3-
"version": "1.9.5",
3+
"version": "1.9.6",
44
"description": "gRPC Library for Node - pure JS implementation",
55
"homepage": "https://grpc.io/",
66
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",

packages/grpc-js/src/load-balancer-pick-first.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,11 @@ export class PickFirstLoadBalancer implements LoadBalancer {
153153
private subchannelStateListener: ConnectivityStateListener = (
154154
subchannel,
155155
previousState,
156-
newState
156+
newState,
157+
keepaliveTime,
158+
errorMessage
157159
) => {
158-
this.onSubchannelStateUpdate(subchannel, previousState, newState);
160+
this.onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage);
159161
};
160162
/**
161163
* Timer reference for the timer tracking when to start
@@ -172,6 +174,12 @@ export class PickFirstLoadBalancer implements LoadBalancer {
172174
*/
173175
private stickyTransientFailureMode = false;
174176

177+
/**
178+
* The most recent error reported by any subchannel as it transitioned to
179+
* TRANSIENT_FAILURE.
180+
*/
181+
private lastError: string | null = null;
182+
175183
/**
176184
* Load balancer that attempts to connect to each backend in the address list
177185
* in order, and picks the first one that connects, using it for every
@@ -200,7 +208,7 @@ export class PickFirstLoadBalancer implements LoadBalancer {
200208
if (this.stickyTransientFailureMode) {
201209
this.updateState(
202210
ConnectivityState.TRANSIENT_FAILURE,
203-
new UnavailablePicker()
211+
new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`})
204212
);
205213
} else {
206214
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
@@ -241,7 +249,8 @@ export class PickFirstLoadBalancer implements LoadBalancer {
241249
private onSubchannelStateUpdate(
242250
subchannel: SubchannelInterface,
243251
previousState: ConnectivityState,
244-
newState: ConnectivityState
252+
newState: ConnectivityState,
253+
errorMessage?: string
245254
) {
246255
if (this.currentPick?.realSubchannelEquals(subchannel)) {
247256
if (newState !== ConnectivityState.READY) {
@@ -258,6 +267,9 @@ export class PickFirstLoadBalancer implements LoadBalancer {
258267
}
259268
if (newState === ConnectivityState.TRANSIENT_FAILURE) {
260269
child.hasReportedTransientFailure = true;
270+
if (errorMessage) {
271+
this.lastError = errorMessage;
272+
}
261273
this.maybeEnterStickyTransientFailureMode();
262274
if (index === this.currentSubchannelIndex) {
263275
this.startNextSubchannelConnecting(index + 1);

packages/grpc-js/src/load-balancer-round-robin.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,24 @@ export class RoundRobinLoadBalancer implements LoadBalancer {
105105

106106
private currentReadyPicker: RoundRobinPicker | null = null;
107107

108+
private lastError: string | null = null;
109+
108110
constructor(private readonly channelControlHelper: ChannelControlHelper) {
109111
this.subchannelStateListener = (
110112
subchannel: SubchannelInterface,
111113
previousState: ConnectivityState,
112-
newState: ConnectivityState
114+
newState: ConnectivityState,
115+
keepaliveTime: number,
116+
errorMessage?: string
113117
) => {
114118
this.calculateAndUpdateState();
115-
116119
if (
117120
newState === ConnectivityState.TRANSIENT_FAILURE ||
118121
newState === ConnectivityState.IDLE
119122
) {
123+
if (errorMessage) {
124+
this.lastError = errorMessage;
125+
}
120126
this.channelControlHelper.requestReresolution();
121127
subchannel.startConnecting();
122128
}
@@ -157,7 +163,7 @@ export class RoundRobinLoadBalancer implements LoadBalancer {
157163
) {
158164
this.updateState(
159165
ConnectivityState.TRANSIENT_FAILURE,
160-
new UnavailablePicker()
166+
new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`})
161167
);
162168
} else {
163169
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));

packages/grpc-js/src/picker.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,13 @@ export interface Picker {
9797
*/
9898
export class UnavailablePicker implements Picker {
9999
private status: StatusObject;
100-
constructor(status?: StatusObject) {
101-
if (status !== undefined) {
102-
this.status = status;
103-
} else {
104-
this.status = {
105-
code: Status.UNAVAILABLE,
106-
details: 'No connection established',
107-
metadata: new Metadata(),
108-
};
109-
}
100+
constructor(status?: Partial<StatusObject>) {
101+
this.status = {
102+
code: Status.UNAVAILABLE,
103+
details: 'No connection established',
104+
metadata: new Metadata(),
105+
...status,
106+
};
110107
}
111108
pick(pickArgs: PickArgs): TransientFailurePickResult {
112109
return {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export type ConnectivityStateListener = (
2323
subchannel: SubchannelInterface,
2424
previousState: ConnectivityState,
2525
newState: ConnectivityState,
26-
keepaliveTime: number
26+
keepaliveTime: number,
27+
errorMessage?: string
2728
) => void;
2829

2930
/**

packages/grpc-js/src/subchannel.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ export class Subchannel {
250250
error => {
251251
this.transitionToState(
252252
[ConnectivityState.CONNECTING],
253-
ConnectivityState.TRANSIENT_FAILURE
253+
ConnectivityState.TRANSIENT_FAILURE,
254+
`${error}`
254255
);
255256
}
256257
);
@@ -265,7 +266,8 @@ export class Subchannel {
265266
*/
266267
private transitionToState(
267268
oldStates: ConnectivityState[],
268-
newState: ConnectivityState
269+
newState: ConnectivityState,
270+
errorMessage?: string
269271
): boolean {
270272
if (oldStates.indexOf(this.connectivityState) === -1) {
271273
return false;
@@ -318,7 +320,7 @@ export class Subchannel {
318320
throw new Error(`Invalid state: unknown ConnectivityState ${newState}`);
319321
}
320322
for (const listener of this.stateListeners) {
321-
listener(this, previousState, newState, this.keepaliveTime);
323+
listener(this, previousState, newState, this.keepaliveTime, errorMessage);
322324
}
323325
return true;
324326
}

packages/grpc-js/src/transport.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ export class Http2SubchannelConnector implements SubchannelConnector {
741741
connectionOptions
742742
);
743743
this.session = session;
744+
let errorMessage = 'Failed to connect';
744745
session.unref();
745746
session.once('connect', () => {
746747
session.removeAllListeners();
@@ -749,10 +750,14 @@ export class Http2SubchannelConnector implements SubchannelConnector {
749750
});
750751
session.once('close', () => {
751752
this.session = null;
752-
reject();
753+
// Leave time for error event to happen before rejecting
754+
setImmediate(() => {
755+
reject(`${errorMessage} (${new Date().toISOString()})`);
756+
});
753757
});
754758
session.once('error', error => {
755-
this.trace('connection failed with error ' + (error as Error).message);
759+
errorMessage = (error as Error).message;
760+
this.trace('connection failed with error ' + errorMessage);
756761
});
757762
});
758763
}

0 commit comments

Comments
 (0)