Skip to content

Commit

Permalink
Merge pull request #2598 from murgatroid99/grpc-js_show_connectivity_…
Browse files Browse the repository at this point in the history
…errors

grpc-js: Propagate connectivity error information to request errors
  • Loading branch information
murgatroid99 authored Oct 17, 2023
2 parents 694230f + 3a9f4d2 commit ebc2c3e
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 24 deletions.
2 changes: 1 addition & 1 deletion packages/grpc-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grpc/grpc-js",
"version": "1.9.5",
"version": "1.9.6",
"description": "gRPC Library for Node - pure JS implementation",
"homepage": "https://grpc.io/",
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",
Expand Down
20 changes: 16 additions & 4 deletions packages/grpc-js/src/load-balancer-pick-first.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,11 @@ export class PickFirstLoadBalancer implements LoadBalancer {
private subchannelStateListener: ConnectivityStateListener = (
subchannel,
previousState,
newState
newState,
keepaliveTime,
errorMessage
) => {
this.onSubchannelStateUpdate(subchannel, previousState, newState);
this.onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage);
};
/**
* Timer reference for the timer tracking when to start
Expand All @@ -172,6 +174,12 @@ export class PickFirstLoadBalancer implements LoadBalancer {
*/
private stickyTransientFailureMode = false;

/**
* The most recent error reported by any subchannel as it transitioned to
* TRANSIENT_FAILURE.
*/
private lastError: string | null = null;

/**
* Load balancer that attempts to connect to each backend in the address list
* in order, and picks the first one that connects, using it for every
Expand Down Expand Up @@ -200,7 +208,7 @@ export class PickFirstLoadBalancer implements LoadBalancer {
if (this.stickyTransientFailureMode) {
this.updateState(
ConnectivityState.TRANSIENT_FAILURE,
new UnavailablePicker()
new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`})
);
} else {
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
Expand Down Expand Up @@ -241,7 +249,8 @@ export class PickFirstLoadBalancer implements LoadBalancer {
private onSubchannelStateUpdate(
subchannel: SubchannelInterface,
previousState: ConnectivityState,
newState: ConnectivityState
newState: ConnectivityState,
errorMessage?: string
) {
if (this.currentPick?.realSubchannelEquals(subchannel)) {
if (newState !== ConnectivityState.READY) {
Expand All @@ -258,6 +267,9 @@ export class PickFirstLoadBalancer implements LoadBalancer {
}
if (newState === ConnectivityState.TRANSIENT_FAILURE) {
child.hasReportedTransientFailure = true;
if (errorMessage) {
this.lastError = errorMessage;
}
this.maybeEnterStickyTransientFailureMode();
if (index === this.currentSubchannelIndex) {
this.startNextSubchannelConnecting(index + 1);
Expand Down
12 changes: 9 additions & 3 deletions packages/grpc-js/src/load-balancer-round-robin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,24 @@ export class RoundRobinLoadBalancer implements LoadBalancer {

private currentReadyPicker: RoundRobinPicker | null = null;

private lastError: string | null = null;

constructor(private readonly channelControlHelper: ChannelControlHelper) {
this.subchannelStateListener = (
subchannel: SubchannelInterface,
previousState: ConnectivityState,
newState: ConnectivityState
newState: ConnectivityState,
keepaliveTime: number,
errorMessage?: string
) => {
this.calculateAndUpdateState();

if (
newState === ConnectivityState.TRANSIENT_FAILURE ||
newState === ConnectivityState.IDLE
) {
if (errorMessage) {
this.lastError = errorMessage;
}
this.channelControlHelper.requestReresolution();
subchannel.startConnecting();
}
Expand Down Expand Up @@ -157,7 +163,7 @@ export class RoundRobinLoadBalancer implements LoadBalancer {
) {
this.updateState(
ConnectivityState.TRANSIENT_FAILURE,
new UnavailablePicker()
new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`})
);
} else {
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
Expand Down
17 changes: 7 additions & 10 deletions packages/grpc-js/src/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,13 @@ export interface Picker {
*/
export class UnavailablePicker implements Picker {
private status: StatusObject;
constructor(status?: StatusObject) {
if (status !== undefined) {
this.status = status;
} else {
this.status = {
code: Status.UNAVAILABLE,
details: 'No connection established',
metadata: new Metadata(),
};
}
constructor(status?: Partial<StatusObject>) {
this.status = {
code: Status.UNAVAILABLE,
details: 'No connection established',
metadata: new Metadata(),
...status,
};
}
pick(pickArgs: PickArgs): TransientFailurePickResult {
return {
Expand Down
3 changes: 2 additions & 1 deletion packages/grpc-js/src/subchannel-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export type ConnectivityStateListener = (
subchannel: SubchannelInterface,
previousState: ConnectivityState,
newState: ConnectivityState,
keepaliveTime: number
keepaliveTime: number,
errorMessage?: string
) => void;

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/grpc-js/src/subchannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ export class Subchannel {
error => {
this.transitionToState(
[ConnectivityState.CONNECTING],
ConnectivityState.TRANSIENT_FAILURE
ConnectivityState.TRANSIENT_FAILURE,
`${error}`
);
}
);
Expand All @@ -265,7 +266,8 @@ export class Subchannel {
*/
private transitionToState(
oldStates: ConnectivityState[],
newState: ConnectivityState
newState: ConnectivityState,
errorMessage?: string
): boolean {
if (oldStates.indexOf(this.connectivityState) === -1) {
return false;
Expand Down Expand Up @@ -318,7 +320,7 @@ export class Subchannel {
throw new Error(`Invalid state: unknown ConnectivityState ${newState}`);
}
for (const listener of this.stateListeners) {
listener(this, previousState, newState, this.keepaliveTime);
listener(this, previousState, newState, this.keepaliveTime, errorMessage);
}
return true;
}
Expand Down
9 changes: 7 additions & 2 deletions packages/grpc-js/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ export class Http2SubchannelConnector implements SubchannelConnector {
connectionOptions
);
this.session = session;
let errorMessage = 'Failed to connect';
session.unref();
session.once('connect', () => {
session.removeAllListeners();
Expand All @@ -749,10 +750,14 @@ export class Http2SubchannelConnector implements SubchannelConnector {
});
session.once('close', () => {
this.session = null;
reject();
// Leave time for error event to happen before rejecting
setImmediate(() => {
reject(`${errorMessage} (${new Date().toISOString()})`);
});
});
session.once('error', error => {
this.trace('connection failed with error ' + (error as Error).message);
errorMessage = (error as Error).message;
this.trace('connection failed with error ' + errorMessage);
});
});
}
Expand Down

0 comments on commit ebc2c3e

Please sign in to comment.