mirror of https://github.com/grpc/grpc-node.git
grpc-js: Fix handling of calls after resolution failure
This commit is contained in:
parent
7664a49a99
commit
6c686772cb
|
|
@ -20,6 +20,7 @@ import {
|
||||||
Call,
|
Call,
|
||||||
Http2CallStream,
|
Http2CallStream,
|
||||||
CallStreamOptions,
|
CallStreamOptions,
|
||||||
|
StatusObject,
|
||||||
} from './call-stream';
|
} from './call-stream';
|
||||||
import { ChannelCredentials } from './channel-credentials';
|
import { ChannelCredentials } from './channel-credentials';
|
||||||
import { ChannelOptions } from './channel-options';
|
import { ChannelOptions } from './channel-options';
|
||||||
|
|
@ -170,6 +171,14 @@ export class ChannelImplementation implements Channel {
|
||||||
*/
|
*/
|
||||||
private callRefTimer: NodeJS.Timer;
|
private callRefTimer: NodeJS.Timer;
|
||||||
private configSelector: ConfigSelector | null = null;
|
private configSelector: ConfigSelector | null = null;
|
||||||
|
/**
|
||||||
|
* This is the error from the name resolver if it failed most recently. It
|
||||||
|
* is only used to end calls that start while there is no config selector
|
||||||
|
* and the name resolver is in backoff, so it should be nulled if
|
||||||
|
* configSelector becomes set or the channel state becomes anything other
|
||||||
|
* than TRANSIENT_FAILURE.
|
||||||
|
*/
|
||||||
|
private currentResolutionError: StatusObject | null = null;
|
||||||
|
|
||||||
// Channelz info
|
// Channelz info
|
||||||
private readonly channelzEnabled: boolean = true;
|
private readonly channelzEnabled: boolean = true;
|
||||||
|
|
@ -290,6 +299,7 @@ export class ChannelImplementation implements Channel {
|
||||||
this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded');
|
this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded');
|
||||||
}
|
}
|
||||||
this.configSelector = configSelector;
|
this.configSelector = configSelector;
|
||||||
|
this.currentResolutionError = null;
|
||||||
/* We process the queue asynchronously to ensure that the corresponding
|
/* We process the queue asynchronously to ensure that the corresponding
|
||||||
* load balancer update has completed. */
|
* load balancer update has completed. */
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
|
@ -309,6 +319,9 @@ export class ChannelImplementation implements Channel {
|
||||||
if (this.configSelectionQueue.length > 0) {
|
if (this.configSelectionQueue.length > 0) {
|
||||||
this.trace('Name resolution failed with calls queued for config selection');
|
this.trace('Name resolution failed with calls queued for config selection');
|
||||||
}
|
}
|
||||||
|
if (this.configSelector === null) {
|
||||||
|
this.currentResolutionError = status;
|
||||||
|
}
|
||||||
const localQueue = this.configSelectionQueue;
|
const localQueue = this.configSelectionQueue;
|
||||||
this.configSelectionQueue = [];
|
this.configSelectionQueue = [];
|
||||||
this.callRefTimerUnref();
|
this.callRefTimerUnref();
|
||||||
|
|
@ -591,6 +604,9 @@ export class ChannelImplementation implements Channel {
|
||||||
watcherObject.callback();
|
watcherObject.callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (newState !== ConnectivityState.TRANSIENT_FAILURE) {
|
||||||
|
this.currentResolutionError = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryGetConfig(stream: Http2CallStream, metadata: Metadata) {
|
private tryGetConfig(stream: Http2CallStream, metadata: Metadata) {
|
||||||
|
|
@ -605,11 +621,15 @@ export class ChannelImplementation implements Channel {
|
||||||
* ResolvingLoadBalancer may be idle and if so it needs to be kicked
|
* ResolvingLoadBalancer may be idle and if so it needs to be kicked
|
||||||
* because it now has a pending request. */
|
* because it now has a pending request. */
|
||||||
this.resolvingLoadBalancer.exitIdle();
|
this.resolvingLoadBalancer.exitIdle();
|
||||||
|
if (this.currentResolutionError && !metadata.getOptions().waitForReady) {
|
||||||
|
stream.cancelWithStatus(this.currentResolutionError.code, this.currentResolutionError.details);
|
||||||
|
} else {
|
||||||
this.configSelectionQueue.push({
|
this.configSelectionQueue.push({
|
||||||
callStream: stream,
|
callStream: stream,
|
||||||
callMetadata: metadata,
|
callMetadata: metadata,
|
||||||
});
|
});
|
||||||
this.callRefTimerRef();
|
this.callRefTimerRef();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const callConfig = this.configSelector(stream.getMethod(), metadata);
|
const callConfig = this.configSelector(stream.getMethod(), metadata);
|
||||||
if (callConfig.status === Status.OK) {
|
if (callConfig.status === Status.OK) {
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,7 @@ export class ResolvingLoadBalancer implements LoadBalancer {
|
||||||
if (this.currentState === ConnectivityState.IDLE) {
|
if (this.currentState === ConnectivityState.IDLE) {
|
||||||
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
|
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
|
||||||
}
|
}
|
||||||
|
this.backoffTimeout.runOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateState(connectivityState: ConnectivityState, picker: Picker) {
|
private updateState(connectivityState: ConnectivityState, picker: Picker) {
|
||||||
|
|
@ -294,18 +295,16 @@ export class ResolvingLoadBalancer implements LoadBalancer {
|
||||||
);
|
);
|
||||||
this.onFailedResolution(error);
|
this.onFailedResolution(error);
|
||||||
}
|
}
|
||||||
this.backoffTimeout.runOnce();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIdle() {
|
exitIdle() {
|
||||||
this.childLoadBalancer.exitIdle();
|
this.childLoadBalancer.exitIdle();
|
||||||
if (this.currentState === ConnectivityState.IDLE) {
|
if (this.currentState === ConnectivityState.IDLE || this.currentState === ConnectivityState.TRANSIENT_FAILURE) {
|
||||||
if (this.backoffTimeout.isRunning()) {
|
if (this.backoffTimeout.isRunning()) {
|
||||||
this.continueResolving = true;
|
this.continueResolving = true;
|
||||||
} else {
|
} else {
|
||||||
this.updateResolution();
|
this.updateResolution();
|
||||||
}
|
}
|
||||||
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,3 +93,27 @@ describe('Client without a server', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Client with a nonexistent target domain', () => {
|
||||||
|
let client: Client;
|
||||||
|
before(() => {
|
||||||
|
// DNS name that does not exist per RFC 6761 section 6.4
|
||||||
|
client = new Client('host.invalid', clientInsecureCreds);
|
||||||
|
});
|
||||||
|
after(() => {
|
||||||
|
client.close();
|
||||||
|
});
|
||||||
|
it('should fail multiple calls', function(done) {
|
||||||
|
this.timeout(5000);
|
||||||
|
// Regression test for https://github.com/grpc/grpc-node/issues/1411
|
||||||
|
client.makeUnaryRequest('/service/method', x => x, x => x, Buffer.from([]), (error, value) => {
|
||||||
|
assert(error);
|
||||||
|
assert.strictEqual(error?.code, grpc.status.UNAVAILABLE);
|
||||||
|
client.makeUnaryRequest('/service/method', x => x, x => x, Buffer.from([]), (error, value) => {
|
||||||
|
assert(error);
|
||||||
|
assert.strictEqual(error?.code, grpc.status.UNAVAILABLE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue