@grpc/grpc-js
Version:
gRPC Library for Node - pure JS implementation
476 lines (441 loc) • 14.3 kB
text/typescript
/*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { ChannelCredentials } from './channel-credentials';
import { Metadata } from './metadata';
import { ChannelOptions } from './channel-options';
import { ConnectivityState } from './connectivity-state';
import { BackoffTimeout, BackoffOptions } from './backoff-timeout';
import * as logging from './logging';
import { LogVerbosity, Status } from './constants';
import { GrpcUri, uriToString } from './uri-parser';
import {
SubchannelAddress,
subchannelAddressToString,
} from './subchannel-address';
import {
SubchannelRef,
ChannelzTrace,
ChannelzChildrenTracker,
SubchannelInfo,
registerChannelzSubchannel,
ChannelzCallTracker,
unregisterChannelzRef,
} from './channelz';
import {
ConnectivityStateListener,
SubchannelInterface,
} from './subchannel-interface';
import { SubchannelCallInterceptingListener } from './subchannel-call';
import { SubchannelCall } from './subchannel-call';
import { CallEventTracker, SubchannelConnector, Transport } from './transport';
const TRACER_NAME = 'subchannel';
/* setInterval and setTimeout only accept signed 32 bit integers. JS doesn't
* have a constant for the max signed 32 bit integer, so this is a simple way
* to calculate it */
const KEEPALIVE_MAX_TIME_MS = ~(1 << 31);
export class Subchannel {
/**
* The subchannel's current connectivity state. Invariant: `session` === `null`
* if and only if `connectivityState` is IDLE or TRANSIENT_FAILURE.
*/
private connectivityState: ConnectivityState = ConnectivityState.IDLE;
/**
* The underlying http2 session used to make requests.
*/
private transport: Transport | null = null;
/**
* Indicates that the subchannel should transition from TRANSIENT_FAILURE to
* CONNECTING instead of IDLE when the backoff timeout ends.
*/
private continueConnecting = false;
/**
* A list of listener functions that will be called whenever the connectivity
* state changes. Will be modified by `addConnectivityStateListener` and
* `removeConnectivityStateListener`
*/
private stateListeners: Set<ConnectivityStateListener> = new Set();
private backoffTimeout: BackoffTimeout;
private keepaliveTime: number;
/**
* Tracks channels and subchannel pools with references to this subchannel
*/
private refcount = 0;
/**
* A string representation of the subchannel address, for logging/tracing
*/
private subchannelAddressString: string;
// Channelz info
private readonly channelzEnabled: boolean = true;
private channelzRef: SubchannelRef;
private channelzTrace: ChannelzTrace;
private callTracker = new ChannelzCallTracker();
private childrenTracker = new ChannelzChildrenTracker();
// Channelz socket info
private streamTracker = new ChannelzCallTracker();
/**
* A class representing a connection to a single backend.
* @param channelTarget The target string for the channel as a whole
* @param subchannelAddress The address for the backend that this subchannel
* will connect to
* @param options The channel options, plus any specific subchannel options
* for this subchannel
* @param credentials The channel credentials used to establish this
* connection
*/
constructor(
private channelTarget: GrpcUri,
private subchannelAddress: SubchannelAddress,
private options: ChannelOptions,
private credentials: ChannelCredentials,
private connector: SubchannelConnector
) {
const backoffOptions: BackoffOptions = {
initialDelay: options['grpc.initial_reconnect_backoff_ms'],
maxDelay: options['grpc.max_reconnect_backoff_ms'],
};
this.backoffTimeout = new BackoffTimeout(() => {
this.handleBackoffTimer();
}, backoffOptions);
this.subchannelAddressString = subchannelAddressToString(subchannelAddress);
this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1;
if (options['grpc.enable_channelz'] === 0) {
this.channelzEnabled = false;
}
this.channelzTrace = new ChannelzTrace();
this.channelzRef = registerChannelzSubchannel(
this.subchannelAddressString,
() => this.getChannelzInfo(),
this.channelzEnabled
);
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Subchannel created');
}
this.trace(
'Subchannel constructed with options ' +
JSON.stringify(options, undefined, 2)
);
}
private getChannelzInfo(): SubchannelInfo {
return {
state: this.connectivityState,
trace: this.channelzTrace,
callTracker: this.callTracker,
children: this.childrenTracker.getChildLists(),
target: this.subchannelAddressString,
};
}
private trace(text: string): void {
logging.trace(
LogVerbosity.DEBUG,
TRACER_NAME,
'(' +
this.channelzRef.id +
') ' +
this.subchannelAddressString +
' ' +
text
);
}
private refTrace(text: string): void {
logging.trace(
LogVerbosity.DEBUG,
'subchannel_refcount',
'(' +
this.channelzRef.id +
') ' +
this.subchannelAddressString +
' ' +
text
);
}
private handleBackoffTimer() {
if (this.continueConnecting) {
this.transitionToState(
[ConnectivityState.TRANSIENT_FAILURE],
ConnectivityState.CONNECTING
);
} else {
this.transitionToState(
[ConnectivityState.TRANSIENT_FAILURE],
ConnectivityState.IDLE
);
}
}
/**
* Start a backoff timer with the current nextBackoff timeout
*/
private startBackoff() {
this.backoffTimeout.runOnce();
}
private stopBackoff() {
this.backoffTimeout.stop();
this.backoffTimeout.reset();
}
private startConnectingInternal() {
let options = this.options;
if (options['grpc.keepalive_time_ms']) {
const adjustedKeepaliveTime = Math.min(
this.keepaliveTime,
KEEPALIVE_MAX_TIME_MS
);
options = { ...options, 'grpc.keepalive_time_ms': adjustedKeepaliveTime };
}
this.connector
.connect(this.subchannelAddress, this.credentials, options)
.then(
transport => {
if (
this.transitionToState(
[ConnectivityState.CONNECTING],
ConnectivityState.READY
)
) {
this.transport = transport;
if (this.channelzEnabled) {
this.childrenTracker.refChild(transport.getChannelzRef());
}
transport.addDisconnectListener(tooManyPings => {
this.transitionToState(
[ConnectivityState.READY],
ConnectivityState.IDLE
);
if (tooManyPings && this.keepaliveTime > 0) {
this.keepaliveTime *= 2;
logging.log(
LogVerbosity.ERROR,
`Connection to ${uriToString(this.channelTarget)} at ${
this.subchannelAddressString
} rejected by server because of excess pings. Increasing ping interval to ${
this.keepaliveTime
} ms`
);
}
});
}
},
error => {
this.transitionToState(
[ConnectivityState.CONNECTING],
ConnectivityState.TRANSIENT_FAILURE
);
}
);
}
/**
* Initiate a state transition from any element of oldStates to the new
* state. If the current connectivityState is not in oldStates, do nothing.
* @param oldStates The set of states to transition from
* @param newState The state to transition to
* @returns True if the state changed, false otherwise
*/
private transitionToState(
oldStates: ConnectivityState[],
newState: ConnectivityState
): boolean {
if (oldStates.indexOf(this.connectivityState) === -1) {
return false;
}
this.trace(
ConnectivityState[this.connectivityState] +
' -> ' +
ConnectivityState[newState]
);
if (this.channelzEnabled) {
this.channelzTrace.addTrace(
'CT_INFO',
'Connectivity state change to ' + ConnectivityState[newState]
);
}
const previousState = this.connectivityState;
this.connectivityState = newState;
switch (newState) {
case ConnectivityState.READY:
this.stopBackoff();
break;
case ConnectivityState.CONNECTING:
this.startBackoff();
this.startConnectingInternal();
this.continueConnecting = false;
break;
case ConnectivityState.TRANSIENT_FAILURE:
if (this.channelzEnabled && this.transport) {
this.childrenTracker.unrefChild(this.transport.getChannelzRef());
}
this.transport?.shutdown();
this.transport = null;
/* If the backoff timer has already ended by the time we get to the
* TRANSIENT_FAILURE state, we want to immediately transition out of
* TRANSIENT_FAILURE as though the backoff timer is ending right now */
if (!this.backoffTimeout.isRunning()) {
process.nextTick(() => {
this.handleBackoffTimer();
});
}
break;
case ConnectivityState.IDLE:
if (this.channelzEnabled && this.transport) {
this.childrenTracker.unrefChild(this.transport.getChannelzRef());
}
this.transport?.shutdown();
this.transport = null;
break;
default:
throw new Error(`Invalid state: unknown ConnectivityState ${newState}`);
}
for (const listener of this.stateListeners) {
listener(this, previousState, newState, this.keepaliveTime);
}
return true;
}
ref() {
this.refTrace('refcount ' + this.refcount + ' -> ' + (this.refcount + 1));
this.refcount += 1;
}
unref() {
this.refTrace('refcount ' + this.refcount + ' -> ' + (this.refcount - 1));
this.refcount -= 1;
if (this.refcount === 0) {
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Shutting down');
}
if (this.channelzEnabled) {
unregisterChannelzRef(this.channelzRef);
}
process.nextTick(() => {
this.transitionToState(
[ConnectivityState.CONNECTING, ConnectivityState.READY],
ConnectivityState.IDLE
);
});
}
}
unrefIfOneRef(): boolean {
if (this.refcount === 1) {
this.unref();
return true;
}
return false;
}
createCall(
metadata: Metadata,
host: string,
method: string,
listener: SubchannelCallInterceptingListener
): SubchannelCall {
if (!this.transport) {
throw new Error('Cannot create call, subchannel not READY');
}
let statsTracker: Partial<CallEventTracker>;
if (this.channelzEnabled) {
this.callTracker.addCallStarted();
this.streamTracker.addCallStarted();
statsTracker = {
onCallEnd: status => {
if (status.code === Status.OK) {
this.callTracker.addCallSucceeded();
} else {
this.callTracker.addCallFailed();
}
},
};
} else {
statsTracker = {};
}
return this.transport.createCall(
metadata,
host,
method,
listener,
statsTracker
);
}
/**
* If the subchannel is currently IDLE, start connecting and switch to the
* CONNECTING state. If the subchannel is current in TRANSIENT_FAILURE,
* the next time it would transition to IDLE, start connecting again instead.
* Otherwise, do nothing.
*/
startConnecting() {
process.nextTick(() => {
/* First, try to transition from IDLE to connecting. If that doesn't happen
* because the state is not currently IDLE, check if it is
* TRANSIENT_FAILURE, and if so indicate that it should go back to
* connecting after the backoff timer ends. Otherwise do nothing */
if (
!this.transitionToState(
[ConnectivityState.IDLE],
ConnectivityState.CONNECTING
)
) {
if (this.connectivityState === ConnectivityState.TRANSIENT_FAILURE) {
this.continueConnecting = true;
}
}
});
}
/**
* Get the subchannel's current connectivity state.
*/
getConnectivityState() {
return this.connectivityState;
}
/**
* Add a listener function to be called whenever the subchannel's
* connectivity state changes.
* @param listener
*/
addConnectivityStateListener(listener: ConnectivityStateListener) {
this.stateListeners.add(listener);
}
/**
* Remove a listener previously added with `addConnectivityStateListener`
* @param listener A reference to a function previously passed to
* `addConnectivityStateListener`
*/
removeConnectivityStateListener(listener: ConnectivityStateListener) {
this.stateListeners.delete(listener);
}
/**
* Reset the backoff timeout, and immediately start connecting if in backoff.
*/
resetBackoff() {
process.nextTick(() => {
this.backoffTimeout.reset();
this.transitionToState(
[ConnectivityState.TRANSIENT_FAILURE],
ConnectivityState.CONNECTING
);
});
}
getAddress(): string {
return this.subchannelAddressString;
}
getChannelzRef(): SubchannelRef {
return this.channelzRef;
}
getRealSubchannel(): this {
return this;
}
realSubchannelEquals(other: SubchannelInterface): boolean {
return other.getRealSubchannel() === this;
}
throttleKeepalive(newKeepaliveTime: number) {
if (newKeepaliveTime > this.keepaliveTime) {
this.keepaliveTime = newKeepaliveTime;
}
}
}