Initial commit

This commit is contained in:
talksik
2021-12-29 01:57:42 -08:00
commit ce39a60b42
4634 changed files with 997667 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright 2021 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 { ServiceDefinition } from "./make-client";
import { Server, UntypedServiceImplementation } from "./server";
interface GetServiceDefinition {
(): ServiceDefinition;
}
interface GetHandlers {
(): UntypedServiceImplementation;
}
const registeredAdminServices: {getServiceDefinition: GetServiceDefinition, getHandlers: GetHandlers}[] = [];
export function registerAdminService(getServiceDefinition: GetServiceDefinition, getHandlers: GetHandlers) {
registeredAdminServices.push({getServiceDefinition, getHandlers});
}
export function addAdminServicesToServer(server: Server): void {
for (const {getServiceDefinition, getHandlers} of registeredAdminServices) {
server.addService(getServiceDefinition(), getHandlers());
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
* 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.
*
*/
const INITIAL_BACKOFF_MS = 1000;
const BACKOFF_MULTIPLIER = 1.6;
const MAX_BACKOFF_MS = 120000;
const BACKOFF_JITTER = 0.2;
/**
* Get a number uniformly at random in the range [min, max)
* @param min
* @param max
*/
function uniformRandom(min: number, max: number) {
return Math.random() * (max - min) + min;
}
export interface BackoffOptions {
initialDelay?: number;
multiplier?: number;
jitter?: number;
maxDelay?: number;
}
export class BackoffTimeout {
private initialDelay: number = INITIAL_BACKOFF_MS;
private multiplier: number = BACKOFF_MULTIPLIER;
private maxDelay: number = MAX_BACKOFF_MS;
private jitter: number = BACKOFF_JITTER;
private nextDelay: number;
private timerId: NodeJS.Timer;
private running = false;
private hasRef = true;
constructor(private callback: () => void, options?: BackoffOptions) {
if (options) {
if (options.initialDelay) {
this.initialDelay = options.initialDelay;
}
if (options.multiplier) {
this.multiplier = options.multiplier;
}
if (options.jitter) {
this.jitter = options.jitter;
}
if (options.maxDelay) {
this.maxDelay = options.maxDelay;
}
}
this.nextDelay = this.initialDelay;
this.timerId = setTimeout(() => {}, 0);
clearTimeout(this.timerId);
}
/**
* Call the callback after the current amount of delay time
*/
runOnce() {
this.running = true;
this.timerId = setTimeout(() => {
this.callback();
this.running = false;
}, this.nextDelay);
if (!this.hasRef) {
this.timerId.unref?.();
}
const nextBackoff = Math.min(
this.nextDelay * this.multiplier,
this.maxDelay
);
const jitterMagnitude = nextBackoff * this.jitter;
this.nextDelay =
nextBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude);
}
/**
* Stop the timer. The callback will not be called until `runOnce` is called
* again.
*/
stop() {
clearTimeout(this.timerId);
this.running = false;
}
/**
* Reset the delay time to its initial value.
*/
reset() {
this.nextDelay = this.initialDelay;
}
isRunning() {
return this.running;
}
ref() {
this.hasRef = true;
this.timerId.ref?.();
}
unref() {
this.hasRef = false;
this.timerId.unref?.();
}
}
+86
View File
@@ -0,0 +1,86 @@
/*
* 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 { Call } from './call-stream';
import { Channel } from './channel';
import { BaseFilter, Filter, FilterFactory } from './filter';
import { Metadata } from './metadata';
import { Status } from './constants';
import { splitHostPort } from './uri-parser';
import { ServiceError } from './call';
export class CallCredentialsFilter extends BaseFilter implements Filter {
private serviceUrl: string;
constructor(
private readonly channel: Channel,
private readonly stream: Call
) {
super();
this.channel = channel;
this.stream = stream;
const splitPath: string[] = stream.getMethod().split('/');
let serviceName = '';
/* The standard path format is "/{serviceName}/{methodName}", so if we split
* by '/', the first item should be empty and the second should be the
* service name */
if (splitPath.length >= 2) {
serviceName = splitPath[1];
}
const hostname = splitHostPort(stream.getHost())?.host ?? 'localhost';
/* Currently, call credentials are only allowed on HTTPS connections, so we
* can assume that the scheme is "https" */
this.serviceUrl = `https://${hostname}/${serviceName}`;
}
async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
const credentials = this.stream.getCredentials();
const credsMetadata = credentials.generateMetadata({
service_url: this.serviceUrl,
});
const resultMetadata = await metadata;
try {
resultMetadata.merge(await credsMetadata);
} catch (error) {
this.stream.cancelWithStatus(
Status.UNAUTHENTICATED,
`Failed to retrieve auth metadata with error: ${error.message}`
);
return Promise.reject<Metadata>('Failed to retrieve auth metadata');
}
if (resultMetadata.get('authorization').length > 1) {
this.stream.cancelWithStatus(
Status.INTERNAL,
'"authorization" metadata cannot have multiple values'
);
return Promise.reject<Metadata>(
'"authorization" metadata cannot have multiple values'
);
}
return resultMetadata;
}
}
export class CallCredentialsFilterFactory
implements FilterFactory<CallCredentialsFilter> {
constructor(private readonly channel: Channel) {
this.channel = channel;
}
createFilter(callStream: Call): CallCredentialsFilter {
return new CallCredentialsFilter(this.channel, callStream);
}
}
+222
View File
@@ -0,0 +1,222 @@
/*
* 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 { Metadata } from './metadata';
export interface CallMetadataOptions {
service_url: string;
}
export type CallMetadataGenerator = (
options: CallMetadataOptions,
cb: (err: Error | null, metadata?: Metadata) => void
) => void;
// google-auth-library pre-v2.0.0 does not have getRequestHeaders
// but has getRequestMetadata, which is deprecated in v2.0.0
export interface OldOAuth2Client {
getRequestMetadata: (
url: string,
callback: (
err: Error | null,
headers?: {
[index: string]: string;
}
) => void
) => void;
}
export interface CurrentOAuth2Client {
getRequestHeaders: (url?: string) => Promise<{ [index: string]: string }>;
}
export type OAuth2Client = OldOAuth2Client | CurrentOAuth2Client;
function isCurrentOauth2Client(
client: OAuth2Client
): client is CurrentOAuth2Client {
return (
'getRequestHeaders' in client &&
typeof client.getRequestHeaders === 'function'
);
}
/**
* A class that represents a generic method of adding authentication-related
* metadata on a per-request basis.
*/
export abstract class CallCredentials {
/**
* Asynchronously generates a new Metadata object.
* @param options Options used in generating the Metadata object.
*/
abstract generateMetadata(options: CallMetadataOptions): Promise<Metadata>;
/**
* Creates a new CallCredentials object from properties of both this and
* another CallCredentials object. This object's metadata generator will be
* called first.
* @param callCredentials The other CallCredentials object.
*/
abstract compose(callCredentials: CallCredentials): CallCredentials;
/**
* Check whether two call credentials objects are equal. Separate
* SingleCallCredentials with identical metadata generator functions are
* equal.
* @param other The other CallCredentials object to compare with.
*/
abstract _equals(other: CallCredentials): boolean;
/**
* Creates a new CallCredentials object from a given function that generates
* Metadata objects.
* @param metadataGenerator A function that accepts a set of options, and
* generates a Metadata object based on these options, which is passed back
* to the caller via a supplied (err, metadata) callback.
*/
static createFromMetadataGenerator(
metadataGenerator: CallMetadataGenerator
): CallCredentials {
return new SingleCallCredentials(metadataGenerator);
}
/**
* Create a gRPC credential from a Google credential object.
* @param googleCredentials The authentication client to use.
* @return The resulting CallCredentials object.
*/
static createFromGoogleCredential(
googleCredentials: OAuth2Client
): CallCredentials {
return CallCredentials.createFromMetadataGenerator((options, callback) => {
let getHeaders: Promise<{ [index: string]: string }>;
if (isCurrentOauth2Client(googleCredentials)) {
getHeaders = googleCredentials.getRequestHeaders(options.service_url);
} else {
getHeaders = new Promise((resolve, reject) => {
googleCredentials.getRequestMetadata(
options.service_url,
(err, headers) => {
if (err) {
reject(err);
return;
}
resolve(headers);
}
);
});
}
getHeaders.then(
(headers) => {
const metadata = new Metadata();
for (const key of Object.keys(headers)) {
metadata.add(key, headers[key]);
}
callback(null, metadata);
},
(err) => {
callback(err);
}
);
});
}
static createEmpty(): CallCredentials {
return new EmptyCallCredentials();
}
}
class ComposedCallCredentials extends CallCredentials {
constructor(private creds: CallCredentials[]) {
super();
}
async generateMetadata(options: CallMetadataOptions): Promise<Metadata> {
const base: Metadata = new Metadata();
const generated: Metadata[] = await Promise.all(
this.creds.map((cred) => cred.generateMetadata(options))
);
for (const gen of generated) {
base.merge(gen);
}
return base;
}
compose(other: CallCredentials): CallCredentials {
return new ComposedCallCredentials(this.creds.concat([other]));
}
_equals(other: CallCredentials): boolean {
if (this === other) {
return true;
}
if (other instanceof ComposedCallCredentials) {
return this.creds.every((value, index) =>
value._equals(other.creds[index])
);
} else {
return false;
}
}
}
class SingleCallCredentials extends CallCredentials {
constructor(private metadataGenerator: CallMetadataGenerator) {
super();
}
generateMetadata(options: CallMetadataOptions): Promise<Metadata> {
return new Promise<Metadata>((resolve, reject) => {
this.metadataGenerator(options, (err, metadata) => {
if (metadata !== undefined) {
resolve(metadata);
} else {
reject(err);
}
});
});
}
compose(other: CallCredentials): CallCredentials {
return new ComposedCallCredentials([this, other]);
}
_equals(other: CallCredentials): boolean {
if (this === other) {
return true;
}
if (other instanceof SingleCallCredentials) {
return this.metadataGenerator === other.metadataGenerator;
} else {
return false;
}
}
}
class EmptyCallCredentials extends CallCredentials {
generateMetadata(options: CallMetadataOptions): Promise<Metadata> {
return Promise.resolve(new Metadata());
}
compose(other: CallCredentials): CallCredentials {
return other;
}
_equals(other: CallCredentials): boolean {
return other instanceof EmptyCallCredentials;
}
}
+869
View File
@@ -0,0 +1,869 @@
/*
* 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 * as http2 from 'http2';
import * as os from 'os';
import { CallCredentials } from './call-credentials';
import { Propagate, Status } from './constants';
import { Filter, FilterFactory } from './filter';
import { FilterStackFactory, FilterStack } from './filter-stack';
import { Metadata } from './metadata';
import { StreamDecoder } from './stream-decoder';
import { ChannelImplementation } from './channel';
import { SubchannelCallStatsTracker, Subchannel } from './subchannel';
import * as logging from './logging';
import { LogVerbosity } from './constants';
import { ServerSurfaceCall } from './server-call';
const TRACER_NAME = 'call_stream';
const {
HTTP2_HEADER_STATUS,
HTTP2_HEADER_CONTENT_TYPE,
NGHTTP2_CANCEL,
} = http2.constants;
/**
* https://nodejs.org/api/errors.html#errors_class_systemerror
*/
interface SystemError extends Error {
address?: string;
code: string;
dest?: string;
errno: number;
info?: object;
message: string;
path?: string;
port?: number;
syscall: string;
}
/**
* Should do approximately the same thing as util.getSystemErrorName but the
* TypeScript types don't have that function for some reason so I just made my
* own.
* @param errno
*/
function getSystemErrorName(errno: number): string {
for (const [name, num] of Object.entries(os.constants.errno)) {
if (num === errno) {
return name;
}
}
return 'Unknown system error ' + errno;
}
export type Deadline = Date | number;
function getMinDeadline(deadlineList: Deadline[]): Deadline {
let minValue = Infinity;
for (const deadline of deadlineList) {
const deadlineMsecs =
deadline instanceof Date ? deadline.getTime() : deadline;
if (deadlineMsecs < minValue) {
minValue = deadlineMsecs;
}
}
return minValue;
}
export interface CallStreamOptions {
deadline: Deadline;
flags: number;
host: string;
parentCall: ServerSurfaceCall | null;
}
export type PartialCallStreamOptions = Partial<CallStreamOptions>;
export interface StatusObject {
code: Status;
details: string;
metadata: Metadata;
}
export const enum WriteFlags {
BufferHint = 1,
NoCompress = 2,
WriteThrough = 4,
}
export interface WriteObject {
message: Buffer;
flags?: number;
}
export interface MetadataListener {
(metadata: Metadata, next: (metadata: Metadata) => void): void;
}
export interface MessageListener {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(message: any, next: (message: any) => void): void;
}
export interface StatusListener {
(status: StatusObject, next: (status: StatusObject) => void): void;
}
export interface FullListener {
onReceiveMetadata: MetadataListener;
onReceiveMessage: MessageListener;
onReceiveStatus: StatusListener;
}
export type Listener = Partial<FullListener>;
/**
* An object with methods for handling the responses to a call.
*/
export interface InterceptingListener {
onReceiveMetadata(metadata: Metadata): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onReceiveMessage(message: any): void;
onReceiveStatus(status: StatusObject): void;
}
export function isInterceptingListener(
listener: Listener | InterceptingListener
): listener is InterceptingListener {
return (
listener.onReceiveMetadata !== undefined &&
listener.onReceiveMetadata.length === 1
);
}
export class InterceptingListenerImpl implements InterceptingListener {
private processingMetadata = false;
private hasPendingMessage = false;
private pendingMessage: any;
private processingMessage = false;
private pendingStatus: StatusObject | null = null;
constructor(
private listener: FullListener,
private nextListener: InterceptingListener
) {}
private processPendingMessage() {
if (this.hasPendingMessage) {
this.nextListener.onReceiveMessage(this.pendingMessage);
this.pendingMessage = null;
this.hasPendingMessage = false;
}
}
private processPendingStatus() {
if (this.pendingStatus) {
this.nextListener.onReceiveStatus(this.pendingStatus);
}
}
onReceiveMetadata(metadata: Metadata): void {
this.processingMetadata = true;
this.listener.onReceiveMetadata(metadata, (metadata) => {
this.processingMetadata = false;
this.nextListener.onReceiveMetadata(metadata);
this.processPendingMessage();
this.processPendingStatus();
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onReceiveMessage(message: any): void {
/* If this listener processes messages asynchronously, the last message may
* be reordered with respect to the status */
this.processingMessage = true;
this.listener.onReceiveMessage(message, (msg) => {
this.processingMessage = false;
if (this.processingMetadata) {
this.pendingMessage = msg;
this.hasPendingMessage = true;
} else {
this.nextListener.onReceiveMessage(msg);
this.processPendingStatus();
}
});
}
onReceiveStatus(status: StatusObject): void {
this.listener.onReceiveStatus(status, (processedStatus) => {
if (this.processingMetadata || this.processingMessage) {
this.pendingStatus = processedStatus;
} else {
this.nextListener.onReceiveStatus(processedStatus);
}
});
}
}
export interface WriteCallback {
(error?: Error | null): void;
}
export interface MessageContext {
callback?: WriteCallback;
flags?: number;
}
export interface Call {
cancelWithStatus(status: Status, details: string): void;
getPeer(): string;
start(metadata: Metadata, listener: InterceptingListener): void;
sendMessageWithContext(context: MessageContext, message: Buffer): void;
startRead(): void;
halfClose(): void;
getDeadline(): Deadline;
getCredentials(): CallCredentials;
setCredentials(credentials: CallCredentials): void;
getMethod(): string;
getHost(): string;
}
export class Http2CallStream implements Call {
credentials: CallCredentials;
filterStack: FilterStack;
private http2Stream: http2.ClientHttp2Stream | null = null;
private pendingRead = false;
private isWriteFilterPending = false;
private pendingWrite: Buffer | null = null;
private pendingWriteCallback: WriteCallback | null = null;
private writesClosed = false;
private decoder = new StreamDecoder();
private isReadFilterPending = false;
private canPush = false;
/**
* Indicates that an 'end' event has come from the http2 stream, so there
* will be no more data events.
*/
private readsClosed = false;
private statusOutput = false;
private unpushedReadMessages: Buffer[] = [];
private unfilteredReadMessages: Buffer[] = [];
// Status code mapped from :status. To be used if grpc-status is not received
private mappedStatusCode: Status = Status.UNKNOWN;
// This is populated (non-null) if and only if the call has ended
private finalStatus: StatusObject | null = null;
private subchannel: Subchannel | null = null;
private disconnectListener: () => void;
private listener: InterceptingListener | null = null;
private internalError: SystemError | null = null;
private configDeadline: Deadline = Infinity;
private statusWatchers: ((status: StatusObject) => void)[] = [];
private streamEndWatchers: ((success: boolean) => void)[] = [];
private callStatsTracker: SubchannelCallStatsTracker | null = null;
constructor(
private readonly methodName: string,
private readonly channel: ChannelImplementation,
private readonly options: CallStreamOptions,
filterStackFactory: FilterStackFactory,
private readonly channelCallCredentials: CallCredentials,
private readonly callNumber: number
) {
this.filterStack = filterStackFactory.createFilter(this);
this.credentials = channelCallCredentials;
this.disconnectListener = () => {
this.endCall({
code: Status.UNAVAILABLE,
details: 'Connection dropped',
metadata: new Metadata(),
});
};
if (
this.options.parentCall &&
this.options.flags & Propagate.CANCELLATION
) {
this.options.parentCall.on('cancelled', () => {
this.cancelWithStatus(Status.CANCELLED, 'Cancelled by parent call');
});
}
}
private outputStatus() {
/* Precondition: this.finalStatus !== null */
if (this.listener && !this.statusOutput) {
this.statusOutput = true;
const filteredStatus = this.filterStack.receiveTrailers(
this.finalStatus!
);
this.statusWatchers.forEach(watcher => watcher(filteredStatus));
/* We delay the actual action of bubbling up the status to insulate the
* cleanup code in this class from any errors that may be thrown in the
* upper layers as a result of bubbling up the status. In particular,
* if the status is not OK, the "error" event may be emitted
* synchronously at the top level, which will result in a thrown error if
* the user does not handle that event. */
process.nextTick(() => {
this.listener?.onReceiveStatus(filteredStatus);
});
if (this.subchannel) {
this.subchannel.callUnref();
this.subchannel.removeDisconnectListener(this.disconnectListener);
}
}
}
private trace(text: string): void {
logging.trace(
LogVerbosity.DEBUG,
TRACER_NAME,
'[' + this.callNumber + '] ' + text
);
}
/**
* On first call, emits a 'status' event with the given StatusObject.
* Subsequent calls are no-ops.
* @param status The status of the call.
*/
private endCall(status: StatusObject): void {
/* If the status is OK and a new status comes in (e.g. from a
* deserialization failure), that new status takes priority */
if (this.finalStatus === null || this.finalStatus.code === Status.OK) {
this.trace(
'ended with status: code=' +
status.code +
' details="' +
status.details +
'"'
);
this.finalStatus = status;
this.maybeOutputStatus();
}
this.destroyHttp2Stream();
}
private maybeOutputStatus() {
if (this.finalStatus !== null) {
/* The combination check of readsClosed and that the two message buffer
* arrays are empty checks that there all incoming data has been fully
* processed */
if (
this.finalStatus.code !== Status.OK ||
(this.readsClosed &&
this.unpushedReadMessages.length === 0 &&
this.unfilteredReadMessages.length === 0 &&
!this.isReadFilterPending)
) {
this.outputStatus();
}
}
}
private push(message: Buffer): void {
this.trace(
'pushing to reader message of length ' +
(message instanceof Buffer ? message.length : null)
);
this.canPush = false;
process.nextTick(() => {
/* If we have already output the status any later messages should be
* ignored, and can cause out-of-order operation errors higher up in the
* stack. Checking as late as possible here to avoid any race conditions.
*/
if (this.statusOutput) {
return;
}
this.listener?.onReceiveMessage(message);
this.maybeOutputStatus();
});
}
private handleFilterError(error: Error) {
this.cancelWithStatus(Status.INTERNAL, error.message);
}
private handleFilteredRead(message: Buffer) {
/* If we the call has already ended with an error, we don't want to do
* anything with this message. Dropping it on the floor is correct
* behavior */
if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) {
this.maybeOutputStatus();
return;
}
this.isReadFilterPending = false;
if (this.canPush) {
this.http2Stream!.pause();
this.push(message);
} else {
this.trace(
'unpushedReadMessages.push message of length ' + message.length
);
this.unpushedReadMessages.push(message);
}
if (this.unfilteredReadMessages.length > 0) {
/* nextMessage is guaranteed not to be undefined because
unfilteredReadMessages is non-empty */
const nextMessage = this.unfilteredReadMessages.shift()!;
this.filterReceivedMessage(nextMessage);
}
}
private filterReceivedMessage(framedMessage: Buffer) {
/* If we the call has already ended with an error, we don't want to do
* anything with this message. Dropping it on the floor is correct
* behavior */
if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) {
this.maybeOutputStatus();
return;
}
this.trace('filterReceivedMessage of length ' + framedMessage.length);
this.isReadFilterPending = true;
this.filterStack
.receiveMessage(Promise.resolve(framedMessage))
.then(
this.handleFilteredRead.bind(this),
this.handleFilterError.bind(this)
);
}
private tryPush(messageBytes: Buffer): void {
if (this.isReadFilterPending) {
this.trace(
'unfilteredReadMessages.push message of length ' +
(messageBytes && messageBytes.length)
);
this.unfilteredReadMessages.push(messageBytes);
} else {
this.filterReceivedMessage(messageBytes);
}
}
private handleTrailers(headers: http2.IncomingHttpHeaders) {
this.streamEndWatchers.forEach(watcher => watcher(true));
let headersString = '';
for (const header of Object.keys(headers)) {
headersString += '\t\t' + header + ': ' + headers[header] + '\n';
}
this.trace('Received server trailers:\n' + headersString);
let metadata: Metadata;
try {
metadata = Metadata.fromHttp2Headers(headers);
} catch (e) {
metadata = new Metadata();
}
const metadataMap = metadata.getMap();
let code: Status = this.mappedStatusCode;
if (
code === Status.UNKNOWN &&
typeof metadataMap['grpc-status'] === 'string'
) {
const receivedStatus = Number(metadataMap['grpc-status']);
if (receivedStatus in Status) {
code = receivedStatus;
this.trace('received status code ' + receivedStatus + ' from server');
}
metadata.remove('grpc-status');
}
let details = '';
if (typeof metadataMap['grpc-message'] === 'string') {
details = decodeURI(metadataMap['grpc-message']);
metadata.remove('grpc-message');
this.trace(
'received status details string "' + details + '" from server'
);
}
const status: StatusObject = { code, details, metadata };
// This is a no-op if the call was already ended when handling headers.
this.endCall(status);
}
private writeMessageToStream(message: Buffer, callback: WriteCallback) {
this.callStatsTracker?.addMessageSent();
this.http2Stream!.write(message, callback);
}
attachHttp2Stream(
stream: http2.ClientHttp2Stream,
subchannel: Subchannel,
extraFilters: Filter[],
callStatsTracker: SubchannelCallStatsTracker
): void {
this.filterStack.push(extraFilters);
if (this.finalStatus !== null) {
stream.close(NGHTTP2_CANCEL);
} else {
this.trace(
'attachHttp2Stream from subchannel ' + subchannel.getAddress()
);
this.http2Stream = stream;
this.subchannel = subchannel;
this.callStatsTracker = callStatsTracker;
subchannel.addDisconnectListener(this.disconnectListener);
subchannel.callRef();
stream.on('response', (headers, flags) => {
let headersString = '';
for (const header of Object.keys(headers)) {
headersString += '\t\t' + header + ': ' + headers[header] + '\n';
}
this.trace('Received server headers:\n' + headersString);
switch (headers[':status']) {
// TODO(murgatroid99): handle 100 and 101
case 400:
this.mappedStatusCode = Status.INTERNAL;
break;
case 401:
this.mappedStatusCode = Status.UNAUTHENTICATED;
break;
case 403:
this.mappedStatusCode = Status.PERMISSION_DENIED;
break;
case 404:
this.mappedStatusCode = Status.UNIMPLEMENTED;
break;
case 429:
case 502:
case 503:
case 504:
this.mappedStatusCode = Status.UNAVAILABLE;
break;
default:
this.mappedStatusCode = Status.UNKNOWN;
}
if (flags & http2.constants.NGHTTP2_FLAG_END_STREAM) {
this.handleTrailers(headers);
} else {
let metadata: Metadata;
try {
metadata = Metadata.fromHttp2Headers(headers);
} catch (error) {
this.endCall({
code: Status.UNKNOWN,
details: error.message,
metadata: new Metadata(),
});
return;
}
try {
const finalMetadata = this.filterStack.receiveMetadata(metadata);
this.listener?.onReceiveMetadata(finalMetadata);
} catch (error) {
this.endCall({
code: Status.UNKNOWN,
details: error.message,
metadata: new Metadata(),
});
}
}
});
stream.on('trailers', this.handleTrailers.bind(this));
stream.on('data', (data: Buffer) => {
this.trace('receive HTTP/2 data frame of length ' + data.length);
const messages = this.decoder.write(data);
for (const message of messages) {
this.trace('parsed message of length ' + message.length);
this.callStatsTracker!.addMessageReceived();
this.tryPush(message);
}
});
stream.on('end', () => {
this.readsClosed = true;
this.maybeOutputStatus();
});
stream.on('close', () => {
/* Use process.next tick to ensure that this code happens after any
* "error" event that may be emitted at about the same time, so that
* we can bubble up the error message from that event. */
process.nextTick(() => {
this.trace('HTTP/2 stream closed with code ' + stream.rstCode);
/* If we have a final status with an OK status code, that means that
* we have received all of the messages and we have processed the
* trailers and the call completed successfully, so it doesn't matter
* how the stream ends after that */
if (this.finalStatus?.code === Status.OK) {
return;
}
let code: Status;
let details = '';
switch (stream.rstCode) {
case http2.constants.NGHTTP2_NO_ERROR:
/* If we get a NO_ERROR code and we already have a status, the
* stream completed properly and we just haven't fully processed
* it yet */
if (this.finalStatus !== null) {
return;
}
code = Status.INTERNAL;
details = `Received RST_STREAM with code ${stream.rstCode}`;
break;
case http2.constants.NGHTTP2_REFUSED_STREAM:
code = Status.UNAVAILABLE;
details = 'Stream refused by server';
break;
case http2.constants.NGHTTP2_CANCEL:
code = Status.CANCELLED;
details = 'Call cancelled';
break;
case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM:
code = Status.RESOURCE_EXHAUSTED;
details = 'Bandwidth exhausted';
break;
case http2.constants.NGHTTP2_INADEQUATE_SECURITY:
code = Status.PERMISSION_DENIED;
details = 'Protocol not secure enough';
break;
case http2.constants.NGHTTP2_INTERNAL_ERROR:
code = Status.INTERNAL;
if (this.internalError === null) {
/* This error code was previously handled in the default case, and
* there are several instances of it online, so I wanted to
* preserve the original error message so that people find existing
* information in searches, but also include the more recognizable
* "Internal server error" message. */
details = `Received RST_STREAM with code ${stream.rstCode} (Internal server error)`;
} else {
if (this.internalError.code === 'ECONNRESET' || this.internalError.code === 'ETIMEDOUT') {
code = Status.UNAVAILABLE;
details = this.internalError.message;
} else {
/* The "Received RST_STREAM with code ..." error is preserved
* here for continuity with errors reported online, but the
* error message at the end will probably be more relevant in
* most cases. */
details = `Received RST_STREAM with code ${stream.rstCode} triggered by internal client error: ${this.internalError.message}`;
}
}
break;
default:
code = Status.INTERNAL;
details = `Received RST_STREAM with code ${stream.rstCode}`;
}
// This is a no-op if trailers were received at all.
// This is OK, because status codes emitted here correspond to more
// catastrophic issues that prevent us from receiving trailers in the
// first place.
this.endCall({ code, details, metadata: new Metadata() });
});
});
stream.on('error', (err: SystemError) => {
/* We need an error handler here to stop "Uncaught Error" exceptions
* from bubbling up. However, errors here should all correspond to
* "close" events, where we will handle the error more granularly */
/* Specifically looking for stream errors that were *not* constructed
* from a RST_STREAM response here:
* https://github.com/nodejs/node/blob/8b8620d580314050175983402dfddf2674e8e22a/lib/internal/http2/core.js#L2267
*/
if (err.code !== 'ERR_HTTP2_STREAM_ERROR') {
this.trace(
'Node error event: message=' +
err.message +
' code=' +
err.code +
' errno=' +
getSystemErrorName(err.errno) +
' syscall=' +
err.syscall
);
this.internalError = err;
}
this.streamEndWatchers.forEach(watcher => watcher(false));
});
if (!this.pendingRead) {
stream.pause();
}
if (this.pendingWrite) {
if (!this.pendingWriteCallback) {
throw new Error('Invalid state in write handling code');
}
this.trace(
'sending data chunk of length ' +
this.pendingWrite.length +
' (deferred)'
);
try {
this.writeMessageToStream(this.pendingWrite, this.pendingWriteCallback);
} catch (error) {
this.endCall({
code: Status.UNAVAILABLE,
details: `Write failed with error ${error.message}`,
metadata: new Metadata()
});
}
}
this.maybeCloseWrites();
}
}
start(metadata: Metadata, listener: InterceptingListener) {
this.trace('Sending metadata');
this.listener = listener;
this.channel._startCallStream(this, metadata);
this.maybeOutputStatus();
}
private destroyHttp2Stream() {
// The http2 stream could already have been destroyed if cancelWithStatus
// is called in response to an internal http2 error.
if (this.http2Stream !== null && !this.http2Stream.destroyed) {
/* If the call has ended with an OK status, communicate that when closing
* the stream, partly to avoid a situation in which we detect an error
* RST_STREAM as a result after we have the status */
let code: number;
if (this.finalStatus?.code === Status.OK) {
code = http2.constants.NGHTTP2_NO_ERROR;
} else {
code = http2.constants.NGHTTP2_CANCEL;
}
this.trace('close http2 stream with code ' + code);
this.http2Stream.close(code);
}
}
cancelWithStatus(status: Status, details: string): void {
this.trace(
'cancelWithStatus code: ' + status + ' details: "' + details + '"'
);
this.endCall({ code: status, details, metadata: new Metadata() });
}
getDeadline(): Deadline {
const deadlineList = [this.options.deadline];
if (this.options.parentCall && this.options.flags & Propagate.DEADLINE) {
deadlineList.push(this.options.parentCall.getDeadline());
}
if (this.configDeadline) {
deadlineList.push(this.configDeadline);
}
return getMinDeadline(deadlineList);
}
getCredentials(): CallCredentials {
return this.credentials;
}
setCredentials(credentials: CallCredentials): void {
this.credentials = this.channelCallCredentials.compose(credentials);
}
getStatus(): StatusObject | null {
return this.finalStatus;
}
getPeer(): string {
return this.subchannel?.getAddress() ?? this.channel.getTarget();
}
getMethod(): string {
return this.methodName;
}
getHost(): string {
return this.options.host;
}
setConfigDeadline(configDeadline: Deadline) {
this.configDeadline = configDeadline;
}
addStatusWatcher(watcher: (status: StatusObject) => void) {
this.statusWatchers.push(watcher);
}
addStreamEndWatcher(watcher: (success: boolean) => void) {
this.streamEndWatchers.push(watcher);
}
addFilters(extraFilters: Filter[]) {
this.filterStack.push(extraFilters);
}
startRead() {
/* If the stream has ended with an error, we should not emit any more
* messages and we should communicate that the stream has ended */
if (this.finalStatus !== null && this.finalStatus.code !== Status.OK) {
this.readsClosed = true;
this.maybeOutputStatus();
return;
}
this.canPush = true;
if (this.http2Stream === null) {
this.pendingRead = true;
} else {
if (this.unpushedReadMessages.length > 0) {
const nextMessage: Buffer = this.unpushedReadMessages.shift()!;
this.push(nextMessage);
return;
}
/* Only resume reading from the http2Stream if we don't have any pending
* messages to emit */
this.http2Stream.resume();
}
}
private maybeCloseWrites() {
if (
this.writesClosed &&
!this.isWriteFilterPending &&
this.http2Stream !== null
) {
this.trace('calling end() on HTTP/2 stream');
this.http2Stream.end();
}
}
sendMessageWithContext(context: MessageContext, message: Buffer) {
this.trace('write() called with message of length ' + message.length);
const writeObj: WriteObject = {
message,
flags: context.flags,
};
const cb: WriteCallback = context.callback ?? (() => {});
this.isWriteFilterPending = true;
this.filterStack.sendMessage(Promise.resolve(writeObj)).then((message) => {
this.isWriteFilterPending = false;
if (this.http2Stream === null) {
this.trace(
'deferring writing data chunk of length ' + message.message.length
);
this.pendingWrite = message.message;
this.pendingWriteCallback = cb;
} else {
this.trace('sending data chunk of length ' + message.message.length);
try {
this.writeMessageToStream(message.message, cb);
} catch (error) {
this.endCall({
code: Status.UNAVAILABLE,
details: `Write failed with error ${error.message}`,
metadata: new Metadata()
});
}
this.maybeCloseWrites();
}
}, this.handleFilterError.bind(this));
}
halfClose() {
this.trace('end() called');
this.writesClosed = true;
this.maybeCloseWrites();
}
}
+193
View File
@@ -0,0 +1,193 @@
/*
* 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 { EventEmitter } from 'events';
import { Duplex, Readable, Writable } from 'stream';
import { StatusObject, MessageContext } from './call-stream';
import { Status } from './constants';
import { EmitterAugmentation1 } from './events';
import { Metadata } from './metadata';
import { ObjectReadable, ObjectWritable, WriteCallback } from './object-stream';
import { InterceptingCallInterface } from './client-interceptors';
/**
* A type extending the built-in Error object with additional fields.
*/
export type ServiceError = StatusObject & Error;
/**
* A base type for all user-facing values returned by client-side method calls.
*/
export type SurfaceCall = {
call?: InterceptingCallInterface;
cancel(): void;
getPeer(): string;
} & EmitterAugmentation1<'metadata', Metadata> &
EmitterAugmentation1<'status', StatusObject> &
EventEmitter;
/**
* A type representing the return value of a unary method call.
*/
export type ClientUnaryCall = SurfaceCall;
/**
* A type representing the return value of a server stream method call.
*/
export type ClientReadableStream<ResponseType> = {
deserialize: (chunk: Buffer) => ResponseType;
} & SurfaceCall &
ObjectReadable<ResponseType>;
/**
* A type representing the return value of a client stream method call.
*/
export type ClientWritableStream<RequestType> = {
serialize: (value: RequestType) => Buffer;
} & SurfaceCall &
ObjectWritable<RequestType>;
/**
* A type representing the return value of a bidirectional stream method call.
*/
export type ClientDuplexStream<
RequestType,
ResponseType
> = ClientWritableStream<RequestType> & ClientReadableStream<ResponseType>;
/**
* Construct a ServiceError from a StatusObject. This function exists primarily
* as an attempt to make the error stack trace clearly communicate that the
* error is not necessarily a problem in gRPC itself.
* @param status
*/
export function callErrorFromStatus(status: StatusObject): ServiceError {
const message = `${status.code} ${Status[status.code]}: ${status.details}`;
return Object.assign(new Error(message), status);
}
export class ClientUnaryCallImpl
extends EventEmitter
implements ClientUnaryCall {
public call?: InterceptingCallInterface;
constructor() {
super();
}
cancel(): void {
this.call?.cancelWithStatus(Status.CANCELLED, 'Cancelled on client');
}
getPeer(): string {
return this.call?.getPeer() ?? 'unknown';
}
}
export class ClientReadableStreamImpl<ResponseType>
extends Readable
implements ClientReadableStream<ResponseType> {
public call?: InterceptingCallInterface;
constructor(readonly deserialize: (chunk: Buffer) => ResponseType) {
super({ objectMode: true });
}
cancel(): void {
this.call?.cancelWithStatus(Status.CANCELLED, 'Cancelled on client');
}
getPeer(): string {
return this.call?.getPeer() ?? 'unknown';
}
_read(_size: number): void {
this.call?.startRead();
}
}
export class ClientWritableStreamImpl<RequestType>
extends Writable
implements ClientWritableStream<RequestType> {
public call?: InterceptingCallInterface;
constructor(readonly serialize: (value: RequestType) => Buffer) {
super({ objectMode: true });
}
cancel(): void {
this.call?.cancelWithStatus(Status.CANCELLED, 'Cancelled on client');
}
getPeer(): string {
return this.call?.getPeer() ?? 'unknown';
}
_write(chunk: RequestType, encoding: string, cb: WriteCallback) {
const context: MessageContext = {
callback: cb,
};
const flags = Number(encoding);
if (!Number.isNaN(flags)) {
context.flags = flags;
}
this.call?.sendMessageWithContext(context, chunk);
}
_final(cb: Function) {
this.call?.halfClose();
cb();
}
}
export class ClientDuplexStreamImpl<RequestType, ResponseType>
extends Duplex
implements ClientDuplexStream<RequestType, ResponseType> {
public call?: InterceptingCallInterface;
constructor(
readonly serialize: (value: RequestType) => Buffer,
readonly deserialize: (chunk: Buffer) => ResponseType
) {
super({ objectMode: true });
}
cancel(): void {
this.call?.cancelWithStatus(Status.CANCELLED, 'Cancelled on client');
}
getPeer(): string {
return this.call?.getPeer() ?? 'unknown';
}
_read(_size: number): void {
this.call?.startRead();
}
_write(chunk: RequestType, encoding: string, cb: WriteCallback) {
const context: MessageContext = {
callback: cb,
};
const flags = Number(encoding);
if (!Number.isNaN(flags)) {
context.flags = flags;
}
this.call?.sendMessageWithContext(context, chunk);
}
_final(cb: Function) {
this.call?.halfClose();
cb();
}
}
+280
View File
@@ -0,0 +1,280 @@
/*
* 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 { ConnectionOptions, createSecureContext, PeerCertificate } from 'tls';
import { CallCredentials } from './call-credentials';
import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function verifyIsBufferOrNull(obj: any, friendlyName: string): void {
if (obj && !(obj instanceof Buffer)) {
throw new TypeError(`${friendlyName}, if provided, must be a Buffer.`);
}
}
/**
* A certificate as received by the checkServerIdentity callback.
*/
export interface Certificate {
/**
* The raw certificate in DER form.
*/
raw: Buffer;
}
/**
* A callback that will receive the expected hostname and presented peer
* certificate as parameters. The callback should return an error to
* indicate that the presented certificate is considered invalid and
* otherwise returned undefined.
*/
export type CheckServerIdentityCallback = (
hostname: string,
cert: Certificate
) => Error | undefined;
function bufferOrNullEqual(buf1: Buffer | null, buf2: Buffer | null) {
if (buf1 === null && buf2 === null) {
return true;
} else {
return buf1 !== null && buf2 !== null && buf1.equals(buf2);
}
}
/**
* Additional peer verification options that can be set when creating
* SSL credentials.
*/
export interface VerifyOptions {
/**
* If set, this callback will be invoked after the usual hostname verification
* has been performed on the peer certificate.
*/
checkServerIdentity?: CheckServerIdentityCallback;
}
/**
* A class that contains credentials for communicating over a channel, as well
* as a set of per-call credentials, which are applied to every method call made
* over a channel initialized with an instance of this class.
*/
export abstract class ChannelCredentials {
protected callCredentials: CallCredentials;
protected constructor(callCredentials?: CallCredentials) {
this.callCredentials = callCredentials || CallCredentials.createEmpty();
}
/**
* Returns a copy of this object with the included set of per-call credentials
* expanded to include callCredentials.
* @param callCredentials A CallCredentials object to associate with this
* instance.
*/
abstract compose(callCredentials: CallCredentials): ChannelCredentials;
/**
* Gets the set of per-call credentials associated with this instance.
*/
_getCallCredentials(): CallCredentials {
return this.callCredentials;
}
/**
* Gets a SecureContext object generated from input parameters if this
* instance was created with createSsl, or null if this instance was created
* with createInsecure.
*/
abstract _getConnectionOptions(): ConnectionOptions | null;
/**
* Indicates whether this credentials object creates a secure channel.
*/
abstract _isSecure(): boolean;
/**
* Check whether two channel credentials objects are equal. Two secure
* credentials are equal if they were constructed with the same parameters.
* @param other The other ChannelCredentials Object
*/
abstract _equals(other: ChannelCredentials): boolean;
/**
* Return a new ChannelCredentials instance with a given set of credentials.
* The resulting instance can be used to construct a Channel that communicates
* over TLS.
* @param rootCerts The root certificate data.
* @param privateKey The client certificate private key, if available.
* @param certChain The client certificate key chain, if available.
*/
static createSsl(
rootCerts?: Buffer | null,
privateKey?: Buffer | null,
certChain?: Buffer | null,
verifyOptions?: VerifyOptions
): ChannelCredentials {
verifyIsBufferOrNull(rootCerts, 'Root certificate');
verifyIsBufferOrNull(privateKey, 'Private key');
verifyIsBufferOrNull(certChain, 'Certificate chain');
if (privateKey && !certChain) {
throw new Error(
'Private key must be given with accompanying certificate chain'
);
}
if (!privateKey && certChain) {
throw new Error(
'Certificate chain must be given with accompanying private key'
);
}
return new SecureChannelCredentialsImpl(
rootCerts || getDefaultRootsData(),
privateKey || null,
certChain || null,
verifyOptions || {}
);
}
/**
* Return a new ChannelCredentials instance with no credentials.
*/
static createInsecure(): ChannelCredentials {
return new InsecureChannelCredentialsImpl();
}
}
class InsecureChannelCredentialsImpl extends ChannelCredentials {
constructor(callCredentials?: CallCredentials) {
super(callCredentials);
}
compose(callCredentials: CallCredentials): ChannelCredentials {
throw new Error('Cannot compose insecure credentials');
}
_getConnectionOptions(): ConnectionOptions | null {
return null;
}
_isSecure(): boolean {
return false;
}
_equals(other: ChannelCredentials): boolean {
return other instanceof InsecureChannelCredentialsImpl;
}
}
class SecureChannelCredentialsImpl extends ChannelCredentials {
connectionOptions: ConnectionOptions;
constructor(
private rootCerts: Buffer | null,
private privateKey: Buffer | null,
private certChain: Buffer | null,
private verifyOptions: VerifyOptions
) {
super();
const secureContext = createSecureContext({
ca: rootCerts || undefined,
key: privateKey || undefined,
cert: certChain || undefined,
ciphers: CIPHER_SUITES,
});
this.connectionOptions = { secureContext };
if (verifyOptions && verifyOptions.checkServerIdentity) {
this.connectionOptions.checkServerIdentity = (
host: string,
cert: PeerCertificate
) => {
return verifyOptions.checkServerIdentity!(host, { raw: cert.raw });
};
}
}
compose(callCredentials: CallCredentials): ChannelCredentials {
const combinedCallCredentials = this.callCredentials.compose(
callCredentials
);
return new ComposedChannelCredentialsImpl(this, combinedCallCredentials);
}
_getConnectionOptions(): ConnectionOptions | null {
// Copy to prevent callers from mutating this.connectionOptions
return { ...this.connectionOptions };
}
_isSecure(): boolean {
return true;
}
_equals(other: ChannelCredentials): boolean {
if (this === other) {
return true;
}
if (other instanceof SecureChannelCredentialsImpl) {
if (!bufferOrNullEqual(this.rootCerts, other.rootCerts)) {
return false;
}
if (!bufferOrNullEqual(this.privateKey, other.privateKey)) {
return false;
}
if (!bufferOrNullEqual(this.certChain, other.certChain)) {
return false;
}
return (
this.verifyOptions.checkServerIdentity ===
other.verifyOptions.checkServerIdentity
);
} else {
return false;
}
}
}
class ComposedChannelCredentialsImpl extends ChannelCredentials {
constructor(
private channelCredentials: SecureChannelCredentialsImpl,
callCreds: CallCredentials
) {
super(callCreds);
}
compose(callCredentials: CallCredentials) {
const combinedCallCredentials = this.callCredentials.compose(
callCredentials
);
return new ComposedChannelCredentialsImpl(
this.channelCredentials,
combinedCallCredentials
);
}
_getConnectionOptions(): ConnectionOptions | null {
return this.channelCredentials._getConnectionOptions();
}
_isSecure(): boolean {
return true;
}
_equals(other: ChannelCredentials): boolean {
if (this === other) {
return true;
}
if (other instanceof ComposedChannelCredentialsImpl) {
return (
this.channelCredentials._equals(other.channelCredentials) &&
this.callCredentials._equals(other.callCredentials)
);
} else {
return false;
}
}
}
+87
View File
@@ -0,0 +1,87 @@
/*
* 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.
*
*/
/**
* An interface that contains options used when initializing a Channel instance.
*/
export interface ChannelOptions {
'grpc.ssl_target_name_override'?: string;
'grpc.primary_user_agent'?: string;
'grpc.secondary_user_agent'?: string;
'grpc.default_authority'?: string;
'grpc.keepalive_time_ms'?: number;
'grpc.keepalive_timeout_ms'?: number;
'grpc.keepalive_permit_without_calls'?: number;
'grpc.service_config'?: string;
'grpc.max_concurrent_streams'?: number;
'grpc.initial_reconnect_backoff_ms'?: number;
'grpc.max_reconnect_backoff_ms'?: number;
'grpc.use_local_subchannel_pool'?: number;
'grpc.max_send_message_length'?: number;
'grpc.max_receive_message_length'?: number;
'grpc.enable_http_proxy'?: number;
'grpc.http_connect_target'?: string;
'grpc.http_connect_creds'?: string;
'grpc.enable_channelz'?: number;
'grpc-node.max_session_memory'?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
/**
* This is for checking provided options at runtime. This is an object for
* easier membership checking.
*/
export const recognizedOptions = {
'grpc.ssl_target_name_override': true,
'grpc.primary_user_agent': true,
'grpc.secondary_user_agent': true,
'grpc.default_authority': true,
'grpc.keepalive_time_ms': true,
'grpc.keepalive_timeout_ms': true,
'grpc.keepalive_permit_without_calls': true,
'grpc.service_config': true,
'grpc.max_concurrent_streams': true,
'grpc.initial_reconnect_backoff_ms': true,
'grpc.max_reconnect_backoff_ms': true,
'grpc.use_local_subchannel_pool': true,
'grpc.max_send_message_length': true,
'grpc.max_receive_message_length': true,
'grpc.enable_http_proxy': true,
'grpc.enable_channelz': true,
'grpc-node.max_session_memory': true,
};
export function channelOptionsEqual(
options1: ChannelOptions,
options2: ChannelOptions
) {
const keys1 = Object.keys(options1).sort();
const keys2 = Object.keys(options2).sort();
if (keys1.length !== keys2.length) {
return false;
}
for (let i = 0; i < keys1.length; i += 1) {
if (keys1[i] !== keys2[i]) {
return false;
}
if (options1[keys1[i]] !== options2[keys2[i]]) {
return false;
}
}
return true;
}
+783
View File
@@ -0,0 +1,783 @@
/*
* 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 {
Deadline,
Call,
Http2CallStream,
CallStreamOptions,
} from './call-stream';
import { ChannelCredentials } from './channel-credentials';
import { ChannelOptions } from './channel-options';
import { ResolvingLoadBalancer } from './resolving-load-balancer';
import { SubchannelPool, getSubchannelPool } from './subchannel-pool';
import { ChannelControlHelper } from './load-balancer';
import { UnavailablePicker, Picker, PickResultType } from './picker';
import { Metadata } from './metadata';
import { Status, LogVerbosity, Propagate } from './constants';
import { FilterStackFactory } from './filter-stack';
import { CallCredentialsFilterFactory } from './call-credentials-filter';
import { DeadlineFilterFactory } from './deadline-filter';
import { CompressionFilterFactory } from './compression-filter';
import {
CallConfig,
ConfigSelector,
getDefaultAuthority,
mapUriDefaultScheme,
} from './resolver';
import { trace, log } from './logging';
import { SubchannelAddress } from './subchannel-address';
import { MaxMessageSizeFilterFactory } from './max-message-size-filter';
import { mapProxyName } from './http_proxy';
import { GrpcUri, parseUri, uriToString } from './uri-parser';
import { ServerSurfaceCall } from './server-call';
import { SurfaceCall } from './call';
import { Filter } from './filter';
import { ConnectivityState } from './connectivity-state';
import { ChannelInfo, ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, SubchannelRef, unregisterChannelzRef } from './channelz';
/**
* See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
*/
const MAX_TIMEOUT_TIME = 2147483647;
let nextCallNumber = 0;
function getNewCallNumber(): number {
const callNumber = nextCallNumber;
nextCallNumber += 1;
if (nextCallNumber >= Number.MAX_SAFE_INTEGER) {
nextCallNumber = 0;
}
return callNumber;
}
/**
* An interface that represents a communication channel to a server specified
* by a given address.
*/
export interface Channel {
/**
* Close the channel. This has the same functionality as the existing
* grpc.Client.prototype.close
*/
close(): void;
/**
* Return the target that this channel connects to
*/
getTarget(): string;
/**
* Get the channel's current connectivity state. This method is here mainly
* because it is in the existing internal Channel class, and there isn't
* another good place to put it.
* @param tryToConnect If true, the channel will start connecting if it is
* idle. Otherwise, idle channels will only start connecting when a
* call starts.
*/
getConnectivityState(tryToConnect: boolean): ConnectivityState;
/**
* Watch for connectivity state changes. This is also here mainly because
* it is in the existing external Channel class.
* @param currentState The state to watch for transitions from. This should
* always be populated by calling getConnectivityState immediately
* before.
* @param deadline A deadline for waiting for a state change
* @param callback Called with no error when a state change, or with an
* error if the deadline passes without a state change.
*/
watchConnectivityState(
currentState: ConnectivityState,
deadline: Date | number,
callback: (error?: Error) => void
): void;
/**
* Get the channelz reference object for this channel. A request to the
* channelz service for the id in this object will provide information
* about this channel.
*/
getChannelzRef(): ChannelRef;
/**
* Create a call object. Call is an opaque type that is used by the Client
* class. This function is called by the gRPC library when starting a
* request. Implementers should return an instance of Call that is returned
* from calling createCall on an instance of the provided Channel class.
* @param method The full method string to request.
* @param deadline The call deadline
* @param host A host string override for making the request
* @param parentCall A server call to propagate some information from
* @param propagateFlags A bitwise combination of elements of grpc.propagate
* that indicates what information to propagate from parentCall.
*/
createCall(
method: string,
deadline: Deadline,
host: string | null | undefined,
parentCall: ServerSurfaceCall | null,
propagateFlags: number | null | undefined
): Call;
}
interface ConnectivityStateWatcher {
currentState: ConnectivityState;
timer: NodeJS.Timeout | null;
callback: (error?: Error) => void;
}
export class ChannelImplementation implements Channel {
private resolvingLoadBalancer: ResolvingLoadBalancer;
private subchannelPool: SubchannelPool;
private connectivityState: ConnectivityState = ConnectivityState.IDLE;
private currentPicker: Picker = new UnavailablePicker();
/**
* Calls queued up to get a call config. Should only be populated before the
* first time the resolver returns a result, which includes the ConfigSelector.
*/
private configSelectionQueue: Array<{
callStream: Http2CallStream;
callMetadata: Metadata;
}> = [];
private pickQueue: Array<{
callStream: Http2CallStream;
callMetadata: Metadata;
callConfig: CallConfig;
dynamicFilters: Filter[];
}> = [];
private connectivityStateWatchers: ConnectivityStateWatcher[] = [];
private defaultAuthority: string;
private filterStackFactory: FilterStackFactory;
private target: GrpcUri;
/**
* This timer does not do anything on its own. Its purpose is to hold the
* event loop open while there are any pending calls for the channel that
* have not yet been assigned to specific subchannels. In other words,
* the invariant is that callRefTimer is reffed if and only if pickQueue
* is non-empty.
*/
private callRefTimer: NodeJS.Timer;
private configSelector: ConfigSelector | null = null;
// Channelz info
private readonly channelzEnabled: boolean = true;
private originalTarget: string;
private channelzRef: ChannelRef;
private channelzTrace: ChannelzTrace;
private callTracker = new ChannelzCallTracker();
private childrenTracker = new ChannelzChildrenTracker();
constructor(
target: string,
private readonly credentials: ChannelCredentials,
private readonly options: ChannelOptions
) {
if (typeof target !== 'string') {
throw new TypeError('Channel target must be a string');
}
if (!(credentials instanceof ChannelCredentials)) {
throw new TypeError(
'Channel credentials must be a ChannelCredentials object'
);
}
if (options) {
if (typeof options !== 'object') {
throw new TypeError('Channel options must be an object');
}
}
this.originalTarget = target;
const originalTargetUri = parseUri(target);
if (originalTargetUri === null) {
throw new Error(`Could not parse target name "${target}"`);
}
/* This ensures that the target has a scheme that is registered with the
* resolver */
const defaultSchemeMapResult = mapUriDefaultScheme(originalTargetUri);
if (defaultSchemeMapResult === null) {
throw new Error(
`Could not find a default scheme for target name "${target}"`
);
}
this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME);
this.callRefTimer.unref?.();
if (this.options['grpc.enable_channelz'] === 0) {
this.channelzEnabled = false;
}
this.channelzTrace = new ChannelzTrace();
if (this.channelzEnabled) {
this.channelzRef = registerChannelzChannel(target, () => this.getChannelzInfo());
this.channelzTrace.addTrace('CT_INFO', 'Channel created');
} else {
// Dummy channelz ref that will never be used
this.channelzRef = {
kind: 'channel',
id: -1,
name: ''
};
}
if (this.options['grpc.default_authority']) {
this.defaultAuthority = this.options['grpc.default_authority'] as string;
} else {
this.defaultAuthority = getDefaultAuthority(defaultSchemeMapResult);
}
const proxyMapResult = mapProxyName(defaultSchemeMapResult, options);
this.target = proxyMapResult.target;
this.options = Object.assign({}, this.options, proxyMapResult.extraOptions);
/* The global boolean parameter to getSubchannelPool has the inverse meaning to what
* the grpc.use_local_subchannel_pool channel option means. */
this.subchannelPool = getSubchannelPool(
(options['grpc.use_local_subchannel_pool'] ?? 0) === 0
);
const channelControlHelper: ChannelControlHelper = {
createSubchannel: (
subchannelAddress: SubchannelAddress,
subchannelArgs: ChannelOptions
) => {
const subchannel = this.subchannelPool.getOrCreateSubchannel(
this.target,
subchannelAddress,
Object.assign({}, this.options, subchannelArgs),
this.credentials
);
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef());
}
return subchannel;
},
updateState: (connectivityState: ConnectivityState, picker: Picker) => {
this.currentPicker = picker;
const queueCopy = this.pickQueue.slice();
this.pickQueue = [];
this.callRefTimerUnref();
for (const { callStream, callMetadata, callConfig, dynamicFilters } of queueCopy) {
this.tryPick(callStream, callMetadata, callConfig, dynamicFilters);
}
this.updateState(connectivityState);
},
requestReresolution: () => {
// This should never be called.
throw new Error(
'Resolving load balancer should never call requestReresolution'
);
},
addChannelzChild: (child: ChannelRef | SubchannelRef) => {
if (this.channelzEnabled) {
this.childrenTracker.refChild(child);
}
},
removeChannelzChild: (child: ChannelRef | SubchannelRef) => {
if (this.channelzEnabled) {
this.childrenTracker.unrefChild(child);
}
}
};
this.resolvingLoadBalancer = new ResolvingLoadBalancer(
this.target,
channelControlHelper,
options,
(configSelector) => {
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Address resolution succeeded');
}
this.configSelector = configSelector;
/* We process the queue asynchronously to ensure that the corresponding
* load balancer update has completed. */
process.nextTick(() => {
const localQueue = this.configSelectionQueue;
this.configSelectionQueue = [];
this.callRefTimerUnref();
for (const { callStream, callMetadata } of localQueue) {
this.tryGetConfig(callStream, callMetadata);
}
this.configSelectionQueue = [];
});
},
(status) => {
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_WARNING', 'Address resolution failed with code ' + status.code + ' and details "' + status.details + '"');
}
if (this.configSelectionQueue.length > 0) {
this.trace('Name resolution failed with calls queued for config selection');
}
const localQueue = this.configSelectionQueue;
this.configSelectionQueue = [];
this.callRefTimerUnref();
for (const { callStream, callMetadata } of localQueue) {
if (callMetadata.getOptions().waitForReady) {
this.callRefTimerRef();
this.configSelectionQueue.push({ callStream, callMetadata });
} else {
callStream.cancelWithStatus(status.code, status.details);
}
}
}
);
this.filterStackFactory = new FilterStackFactory([
new CallCredentialsFilterFactory(this),
new DeadlineFilterFactory(this),
new MaxMessageSizeFilterFactory(this.options),
new CompressionFilterFactory(this),
]);
this.trace('Channel constructed with options ' + JSON.stringify(options, undefined, 2));
}
private getChannelzInfo(): ChannelInfo {
return {
target: this.originalTarget,
state: this.connectivityState,
trace: this.channelzTrace,
callTracker: this.callTracker,
children: this.childrenTracker.getChildLists()
};
}
private trace(text: string, verbosityOverride?: LogVerbosity) {
trace(verbosityOverride ?? LogVerbosity.DEBUG, 'channel', '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + text);
}
private callRefTimerRef() {
// If the hasRef function does not exist, always run the code
if (!this.callRefTimer.hasRef?.()) {
this.trace(
'callRefTimer.ref | configSelectionQueue.length=' +
this.configSelectionQueue.length +
' pickQueue.length=' +
this.pickQueue.length
);
this.callRefTimer.ref?.();
}
}
private callRefTimerUnref() {
// If the hasRef function does not exist, always run the code
if (!this.callRefTimer.hasRef || this.callRefTimer.hasRef()) {
this.trace(
'callRefTimer.unref | configSelectionQueue.length=' +
this.configSelectionQueue.length +
' pickQueue.length=' +
this.pickQueue.length
);
this.callRefTimer.unref?.();
}
}
private pushPick(
callStream: Http2CallStream,
callMetadata: Metadata,
callConfig: CallConfig,
dynamicFilters: Filter[]
) {
this.pickQueue.push({ callStream, callMetadata, callConfig, dynamicFilters });
this.callRefTimerRef();
}
/**
* Check the picker output for the given call and corresponding metadata,
* and take any relevant actions. Should not be called while iterating
* over pickQueue.
* @param callStream
* @param callMetadata
*/
private tryPick(
callStream: Http2CallStream,
callMetadata: Metadata,
callConfig: CallConfig,
dynamicFilters: Filter[]
) {
const pickResult = this.currentPicker.pick({
metadata: callMetadata,
extraPickInfo: callConfig.pickInformation,
});
this.trace(
'Pick result: ' +
PickResultType[pickResult.pickResultType] +
' subchannel: ' +
pickResult.subchannel?.getAddress() +
' status: ' +
pickResult.status?.code +
' ' +
pickResult.status?.details
);
switch (pickResult.pickResultType) {
case PickResultType.COMPLETE:
if (pickResult.subchannel === null) {
callStream.cancelWithStatus(
Status.UNAVAILABLE,
'Request dropped by load balancing policy'
);
// End the call with an error
} else {
/* If the subchannel is not in the READY state, that indicates a bug
* somewhere in the load balancer or picker. So, we log an error and
* queue the pick to be tried again later. */
if (
pickResult.subchannel!.getConnectivityState() !==
ConnectivityState.READY
) {
log(
LogVerbosity.ERROR,
'Error: COMPLETE pick result subchannel ' +
pickResult.subchannel!.getAddress() +
' has state ' +
ConnectivityState[pickResult.subchannel!.getConnectivityState()]
);
this.pushPick(callStream, callMetadata, callConfig, dynamicFilters);
break;
}
/* We need to clone the callMetadata here because the transparent
* retry code in the promise resolution handler use the same
* callMetadata object, so it needs to stay unmodified */
callStream.filterStack
.sendMetadata(Promise.resolve(callMetadata.clone()))
.then(
(finalMetadata) => {
const subchannelState: ConnectivityState = pickResult.subchannel!.getConnectivityState();
if (subchannelState === ConnectivityState.READY) {
try {
const pickExtraFilters = pickResult.extraFilterFactories.map(factory => factory.createFilter(callStream));
pickResult.subchannel!.startCallStream(
finalMetadata,
callStream,
[...dynamicFilters, ...pickExtraFilters]
);
/* If we reach this point, the call stream has started
* successfully */
callConfig.onCommitted?.();
pickResult.onCallStarted?.();
} catch (error) {
if (
(error as NodeJS.ErrnoException).code ===
'ERR_HTTP2_GOAWAY_SESSION'
) {
/* An error here indicates that something went wrong with
* the picked subchannel's http2 stream right before we
* tried to start the stream. We are handling a promise
* result here, so this is asynchronous with respect to the
* original tryPick call, so calling it again is not
* recursive. We call tryPick immediately instead of
* queueing this pick again because handling the queue is
* triggered by state changes, and we want to immediately
* check if the state has already changed since the
* previous tryPick call. We do this instead of cancelling
* the stream because the correct behavior may be
* re-queueing instead, based on the logic in the rest of
* tryPick */
this.trace(
'Failed to start call on picked subchannel ' +
pickResult.subchannel!.getAddress() +
' with error ' +
(error as Error).message +
'. Retrying pick',
LogVerbosity.INFO
);
this.tryPick(callStream, callMetadata, callConfig, dynamicFilters);
} else {
this.trace(
'Failed to start call on picked subchanel ' +
pickResult.subchannel!.getAddress() +
' with error ' +
(error as Error).message +
'. Ending call',
LogVerbosity.INFO
);
callStream.cancelWithStatus(
Status.INTERNAL,
`Failed to start HTTP/2 stream with error: ${
(error as Error).message
}`
);
}
}
} else {
/* The logic for doing this here is the same as in the catch
* block above */
this.trace(
'Picked subchannel ' +
pickResult.subchannel!.getAddress() +
' has state ' +
ConnectivityState[subchannelState] +
' after metadata filters. Retrying pick',
LogVerbosity.INFO
);
this.tryPick(callStream, callMetadata, callConfig, dynamicFilters);
}
},
(error: Error & { code: number }) => {
// We assume the error code isn't 0 (Status.OK)
callStream.cancelWithStatus(
typeof error.code === 'number' ? error.code : Status.UNKNOWN,
`Getting metadata from plugin failed with error: ${error.message}`
);
}
);
}
break;
case PickResultType.QUEUE:
this.pushPick(callStream, callMetadata, callConfig, dynamicFilters);
break;
case PickResultType.TRANSIENT_FAILURE:
if (callMetadata.getOptions().waitForReady) {
this.pushPick(callStream, callMetadata, callConfig, dynamicFilters);
} else {
callStream.cancelWithStatus(
pickResult.status!.code,
pickResult.status!.details
);
}
break;
case PickResultType.DROP:
callStream.cancelWithStatus(
pickResult.status!.code,
pickResult.status!.details
);
break;
default:
throw new Error(
`Invalid state: unknown pickResultType ${pickResult.pickResultType}`
);
}
}
private removeConnectivityStateWatcher(
watcherObject: ConnectivityStateWatcher
) {
const watcherIndex = this.connectivityStateWatchers.findIndex(
(value) => value === watcherObject
);
if (watcherIndex >= 0) {
this.connectivityStateWatchers.splice(watcherIndex, 1);
}
}
private updateState(newState: ConnectivityState): void {
trace(
LogVerbosity.DEBUG,
'connectivity_state',
'(' + this.channelzRef.id + ') ' +
uriToString(this.target) +
' ' +
ConnectivityState[this.connectivityState] +
' -> ' +
ConnectivityState[newState]
);
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', ConnectivityState[this.connectivityState] + ' -> ' + ConnectivityState[newState]);
}
this.connectivityState = newState;
const watchersCopy = this.connectivityStateWatchers.slice();
for (const watcherObject of watchersCopy) {
if (newState !== watcherObject.currentState) {
if (watcherObject.timer) {
clearTimeout(watcherObject.timer);
}
this.removeConnectivityStateWatcher(watcherObject);
watcherObject.callback();
}
}
}
private tryGetConfig(stream: Http2CallStream, metadata: Metadata) {
if (stream.getStatus() !== null) {
/* If the stream has a status, it has already finished and we don't need
* to take any more actions on it. */
return;
}
if (this.configSelector === null) {
/* This branch will only be taken at the beginning of the channel's life,
* before the resolver ever returns a result. So, the
* ResolvingLoadBalancer may be idle and if so it needs to be kicked
* because it now has a pending request. */
this.resolvingLoadBalancer.exitIdle();
this.configSelectionQueue.push({
callStream: stream,
callMetadata: metadata,
});
this.callRefTimerRef();
} else {
const callConfig = this.configSelector(stream.getMethod(), metadata);
if (callConfig.status === Status.OK) {
if (callConfig.methodConfig.timeout) {
const deadline = new Date();
deadline.setSeconds(
deadline.getSeconds() + callConfig.methodConfig.timeout.seconds
);
deadline.setMilliseconds(
deadline.getMilliseconds() +
callConfig.methodConfig.timeout.nanos / 1_000_000
);
stream.setConfigDeadline(deadline);
// Refreshing the filters makes the deadline filter pick up the new deadline
stream.filterStack.refresh();
}
if (callConfig.dynamicFilterFactories.length > 0) {
/* These dynamicFilters are the mechanism for implementing gRFC A39:
* https://github.com/grpc/proposal/blob/master/A39-xds-http-filters.md
* We run them here instead of with the rest of the filters because
* that spec says "the xDS HTTP filters will run in between name
* resolution and load balancing".
*
* We use the filter stack here to simplify the multi-filter async
* waterfall logic, but we pass along the underlying list of filters
* to avoid having nested filter stacks when combining it with the
* original filter stack. We do not pass along the original filter
* factory list because these filters may need to persist data
* between sending headers and other operations. */
const dynamicFilterStackFactory = new FilterStackFactory(callConfig.dynamicFilterFactories);
const dynamicFilterStack = dynamicFilterStackFactory.createFilter(stream);
dynamicFilterStack.sendMetadata(Promise.resolve(metadata)).then(filteredMetadata => {
this.tryPick(stream, filteredMetadata, callConfig, dynamicFilterStack.getFilters());
});
} else {
this.tryPick(stream, metadata, callConfig, []);
}
} else {
stream.cancelWithStatus(
callConfig.status,
'Failed to route call to method ' + stream.getMethod()
);
}
}
}
_startCallStream(stream: Http2CallStream, metadata: Metadata) {
this.tryGetConfig(stream, metadata.clone());
}
close() {
this.resolvingLoadBalancer.destroy();
this.updateState(ConnectivityState.SHUTDOWN);
clearInterval(this.callRefTimer);
if (this.channelzEnabled) {
unregisterChannelzRef(this.channelzRef);
}
this.subchannelPool.unrefUnusedSubchannels();
}
getTarget() {
return uriToString(this.target);
}
getConnectivityState(tryToConnect: boolean) {
const connectivityState = this.connectivityState;
if (tryToConnect) {
this.resolvingLoadBalancer.exitIdle();
}
return connectivityState;
}
watchConnectivityState(
currentState: ConnectivityState,
deadline: Date | number,
callback: (error?: Error) => void
): void {
if (this.connectivityState === ConnectivityState.SHUTDOWN) {
throw new Error('Channel has been shut down');
}
let timer = null;
if (deadline !== Infinity) {
const deadlineDate: Date =
deadline instanceof Date ? deadline : new Date(deadline);
const now = new Date();
if (deadline === -Infinity || deadlineDate <= now) {
process.nextTick(
callback,
new Error('Deadline passed without connectivity state change')
);
return;
}
timer = setTimeout(() => {
this.removeConnectivityStateWatcher(watcherObject);
callback(
new Error('Deadline passed without connectivity state change')
);
}, deadlineDate.getTime() - now.getTime());
}
const watcherObject = {
currentState,
callback,
timer,
};
this.connectivityStateWatchers.push(watcherObject);
}
/**
* Get the channelz reference object for this channel. The returned value is
* garbage if channelz is disabled for this channel.
* @returns
*/
getChannelzRef() {
return this.channelzRef;
}
createCall(
method: string,
deadline: Deadline,
host: string | null | undefined,
parentCall: ServerSurfaceCall | null,
propagateFlags: number | null | undefined
): Call {
if (typeof method !== 'string') {
throw new TypeError('Channel#createCall: method must be a string');
}
if (!(typeof deadline === 'number' || deadline instanceof Date)) {
throw new TypeError(
'Channel#createCall: deadline must be a number or Date'
);
}
if (this.connectivityState === ConnectivityState.SHUTDOWN) {
throw new Error('Channel has been shut down');
}
const callNumber = getNewCallNumber();
this.trace(
'createCall [' +
callNumber +
'] method="' +
method +
'", deadline=' +
deadline
);
const finalOptions: CallStreamOptions = {
deadline: deadline,
flags: propagateFlags ?? Propagate.DEFAULTS,
host: host ?? this.defaultAuthority,
parentCall: parentCall,
};
const stream: Http2CallStream = new Http2CallStream(
method,
this,
finalOptions,
this.filterStackFactory,
this.credentials._getCallCredentials(),
callNumber
);
if (this.channelzEnabled) {
this.callTracker.addCallStarted();
stream.addStatusWatcher(status => {
if (status.code === Status.OK) {
this.callTracker.addCallSucceeded();
} else {
this.callTracker.addCallFailed();
}
});
}
return stream;
}
}
+750
View File
@@ -0,0 +1,750 @@
/*
* Copyright 2021 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 { isIPv4, isIPv6 } from "net";
import { ConnectivityState } from "./connectivity-state";
import { Status } from "./constants";
import { Timestamp } from "./generated/google/protobuf/Timestamp";
import { Channel as ChannelMessage } from "./generated/grpc/channelz/v1/Channel";
import { ChannelConnectivityState__Output } from "./generated/grpc/channelz/v1/ChannelConnectivityState";
import { ChannelRef as ChannelRefMessage } from "./generated/grpc/channelz/v1/ChannelRef";
import { ChannelTrace } from "./generated/grpc/channelz/v1/ChannelTrace";
import { GetChannelRequest__Output } from "./generated/grpc/channelz/v1/GetChannelRequest";
import { GetChannelResponse } from "./generated/grpc/channelz/v1/GetChannelResponse";
import { sendUnaryData, ServerUnaryCall } from "./server-call";
import { ServerRef as ServerRefMessage } from "./generated/grpc/channelz/v1/ServerRef";
import { SocketRef as SocketRefMessage } from "./generated/grpc/channelz/v1/SocketRef";
import { isTcpSubchannelAddress, SubchannelAddress } from "./subchannel-address";
import { SubchannelRef as SubchannelRefMessage } from "./generated/grpc/channelz/v1/SubchannelRef";
import { GetServerRequest__Output } from "./generated/grpc/channelz/v1/GetServerRequest";
import { GetServerResponse } from "./generated/grpc/channelz/v1/GetServerResponse";
import { Server as ServerMessage } from "./generated/grpc/channelz/v1/Server";
import { GetServersRequest__Output } from "./generated/grpc/channelz/v1/GetServersRequest";
import { GetServersResponse } from "./generated/grpc/channelz/v1/GetServersResponse";
import { GetTopChannelsRequest__Output } from "./generated/grpc/channelz/v1/GetTopChannelsRequest";
import { GetTopChannelsResponse } from "./generated/grpc/channelz/v1/GetTopChannelsResponse";
import { GetSubchannelRequest__Output } from "./generated/grpc/channelz/v1/GetSubchannelRequest";
import { GetSubchannelResponse } from "./generated/grpc/channelz/v1/GetSubchannelResponse";
import { Subchannel as SubchannelMessage } from "./generated/grpc/channelz/v1/Subchannel";
import { GetSocketRequest__Output } from "./generated/grpc/channelz/v1/GetSocketRequest";
import { GetSocketResponse } from "./generated/grpc/channelz/v1/GetSocketResponse";
import { Socket as SocketMessage } from "./generated/grpc/channelz/v1/Socket";
import { Address } from "./generated/grpc/channelz/v1/Address";
import { Security } from "./generated/grpc/channelz/v1/Security";
import { GetServerSocketsRequest__Output } from "./generated/grpc/channelz/v1/GetServerSocketsRequest";
import { GetServerSocketsResponse } from "./generated/grpc/channelz/v1/GetServerSocketsResponse";
import { ChannelzDefinition, ChannelzHandlers } from "./generated/grpc/channelz/v1/Channelz";
import { ProtoGrpcType as ChannelzProtoGrpcType } from "./generated/channelz";
import type { loadSync } from '@grpc/proto-loader';
import { registerAdminService } from "./admin";
import { loadPackageDefinition } from "./make-client";
export type TraceSeverity = 'CT_UNKNOWN' | 'CT_INFO' | 'CT_WARNING' | 'CT_ERROR';
export interface ChannelRef {
kind: 'channel';
id: number;
name: string;
}
export interface SubchannelRef {
kind: 'subchannel';
id: number;
name: string;
}
export interface ServerRef {
kind: 'server';
id: number;
}
export interface SocketRef {
kind: 'socket';
id: number;
name: string;
}
function channelRefToMessage(ref: ChannelRef): ChannelRefMessage {
return {
channel_id: ref.id,
name: ref.name
};
}
function subchannelRefToMessage(ref: SubchannelRef): SubchannelRefMessage {
return {
subchannel_id: ref.id,
name: ref.name
}
}
function serverRefToMessage(ref: ServerRef): ServerRefMessage {
return {
server_id: ref.id
}
}
function socketRefToMessage(ref: SocketRef): SocketRefMessage {
return {
socket_id: ref.id,
name: ref.name
}
}
interface TraceEvent {
description: string;
severity: TraceSeverity;
timestamp: Date;
childChannel?: ChannelRef;
childSubchannel?: SubchannelRef;
}
/**
* The loose upper bound on the number of events that should be retained in a
* trace. This may be exceeded by up to a factor of 2. Arbitrarily chosen as a
* number that should be large enough to contain the recent relevant
* information, but small enough to not use excessive memory.
*/
const TARGET_RETAINED_TRACES = 32;
export class ChannelzTrace {
events: TraceEvent[] = [];
creationTimestamp: Date;
eventsLogged: number = 0;
constructor() {
this.creationTimestamp = new Date();
}
addTrace(severity: TraceSeverity, description: string, child?: ChannelRef | SubchannelRef) {
const timestamp = new Date();
this.events.push({
description: description,
severity: severity,
timestamp: timestamp,
childChannel: child?.kind === 'channel' ? child : undefined,
childSubchannel: child?.kind === 'subchannel' ? child : undefined
});
// Whenever the trace array gets too large, discard the first half
if (this.events.length >= TARGET_RETAINED_TRACES * 2) {
this.events = this.events.slice(TARGET_RETAINED_TRACES);
}
this.eventsLogged += 1;
}
getTraceMessage(): ChannelTrace {
return {
creation_timestamp: dateToProtoTimestamp(this.creationTimestamp),
num_events_logged: this.eventsLogged,
events: this.events.map(event => {
return {
description: event.description,
severity: event.severity,
timestamp: dateToProtoTimestamp(event.timestamp),
channel_ref: event.childChannel ? channelRefToMessage(event.childChannel) : null,
subchannel_ref: event.childSubchannel ? subchannelRefToMessage(event.childSubchannel) : null
}
})
};
}
}
export class ChannelzChildrenTracker {
private channelChildren: Map<number, {ref: ChannelRef, count: number}> = new Map<number, {ref: ChannelRef, count: number}>();
private subchannelChildren: Map<number, {ref: SubchannelRef, count: number}> = new Map<number, {ref: SubchannelRef, count: number}>();
private socketChildren: Map<number, {ref: SocketRef, count: number}> = new Map<number, {ref: SocketRef, count: number}>();
refChild(child: ChannelRef | SubchannelRef | SocketRef) {
switch (child.kind) {
case 'channel': {
let trackedChild = this.channelChildren.get(child.id) ?? {ref: child, count: 0};
trackedChild.count += 1;
this.channelChildren.set(child.id, trackedChild);
break;
}
case 'subchannel':{
let trackedChild = this.subchannelChildren.get(child.id) ?? {ref: child, count: 0};
trackedChild.count += 1;
this.subchannelChildren.set(child.id, trackedChild);
break;
}
case 'socket':{
let trackedChild = this.socketChildren.get(child.id) ?? {ref: child, count: 0};
trackedChild.count += 1;
this.socketChildren.set(child.id, trackedChild);
break;
}
}
}
unrefChild(child: ChannelRef | SubchannelRef | SocketRef) {
switch (child.kind) {
case 'channel': {
let trackedChild = this.channelChildren.get(child.id);
if (trackedChild !== undefined) {
trackedChild.count -= 1;
if (trackedChild.count === 0) {
this.channelChildren.delete(child.id);
} else {
this.channelChildren.set(child.id, trackedChild);
}
}
break;
}
case 'subchannel': {
let trackedChild = this.subchannelChildren.get(child.id);
if (trackedChild !== undefined) {
trackedChild.count -= 1;
if (trackedChild.count === 0) {
this.subchannelChildren.delete(child.id);
} else {
this.subchannelChildren.set(child.id, trackedChild);
}
}
break;
}
case 'socket': {
let trackedChild = this.socketChildren.get(child.id);
if (trackedChild !== undefined) {
trackedChild.count -= 1;
if (trackedChild.count === 0) {
this.socketChildren.delete(child.id);
} else {
this.socketChildren.set(child.id, trackedChild);
}
}
break;
}
}
}
getChildLists(): ChannelzChildren {
const channels: ChannelRef[] = [];
for (const {ref} of this.channelChildren.values()) {
channels.push(ref);
}
const subchannels: SubchannelRef[] = [];
for (const {ref} of this.subchannelChildren.values()) {
subchannels.push(ref);
}
const sockets: SocketRef[] = [];
for (const {ref} of this.socketChildren.values()) {
sockets.push(ref);
}
return {channels, subchannels, sockets};
}
}
export class ChannelzCallTracker {
callsStarted: number = 0;
callsSucceeded: number = 0;
callsFailed: number = 0;
lastCallStartedTimestamp: Date | null = null;
addCallStarted() {
this.callsStarted += 1;
this.lastCallStartedTimestamp = new Date();
}
addCallSucceeded() {
this.callsSucceeded += 1;
}
addCallFailed() {
this.callsFailed += 1;
}
}
export interface ChannelzChildren {
channels: ChannelRef[];
subchannels: SubchannelRef[];
sockets: SocketRef[];
}
export interface ChannelInfo {
target: string;
state: ConnectivityState;
trace: ChannelzTrace;
callTracker: ChannelzCallTracker;
children: ChannelzChildren;
}
export interface SubchannelInfo extends ChannelInfo {}
export interface ServerInfo {
trace: ChannelzTrace;
callTracker: ChannelzCallTracker;
listenerChildren: ChannelzChildren;
sessionChildren: ChannelzChildren;
}
export interface TlsInfo {
cipherSuiteStandardName: string | null;
cipherSuiteOtherName: string | null;
localCertificate: Buffer | null;
remoteCertificate: Buffer | null;
}
export interface SocketInfo {
localAddress: SubchannelAddress | null;
remoteAddress: SubchannelAddress | null;
security: TlsInfo | null;
remoteName: string | null;
streamsStarted: number;
streamsSucceeded: number;
streamsFailed: number;
messagesSent: number;
messagesReceived: number;
keepAlivesSent: number;
lastLocalStreamCreatedTimestamp: Date | null;
lastRemoteStreamCreatedTimestamp: Date | null;
lastMessageSentTimestamp: Date | null;
lastMessageReceivedTimestamp: Date | null;
localFlowControlWindow: number | null;
remoteFlowControlWindow: number | null;
}
interface ChannelEntry {
ref: ChannelRef;
getInfo(): ChannelInfo;
}
interface SubchannelEntry {
ref: SubchannelRef;
getInfo(): SubchannelInfo;
}
interface ServerEntry {
ref: ServerRef;
getInfo(): ServerInfo;
}
interface SocketEntry {
ref: SocketRef;
getInfo(): SocketInfo;
}
let nextId = 1;
function getNextId(): number {
return nextId++;
}
const channels: (ChannelEntry | undefined)[] = [];
const subchannels: (SubchannelEntry | undefined)[] = [];
const servers: (ServerEntry | undefined)[] = [];
const sockets: (SocketEntry | undefined)[] = [];
export function registerChannelzChannel(name: string, getInfo: () => ChannelInfo): ChannelRef {
const id = getNextId();
const ref: ChannelRef = {id, name, kind: 'channel'};
channels[id] = { ref, getInfo };
return ref;
}
export function registerChannelzSubchannel(name: string, getInfo:() => SubchannelInfo): SubchannelRef {
const id = getNextId();
const ref: SubchannelRef = {id, name, kind: 'subchannel'};
subchannels[id] = { ref, getInfo };
return ref;
}
export function registerChannelzServer(getInfo: () => ServerInfo): ServerRef {
const id = getNextId();
const ref: ServerRef = {id, kind: 'server'};
servers[id] = { ref, getInfo };
return ref;
}
export function registerChannelzSocket(name: string, getInfo: () => SocketInfo): SocketRef {
const id = getNextId();
const ref: SocketRef = {id, name, kind: 'socket'};
sockets[id] = { ref, getInfo};
return ref;
}
export function unregisterChannelzRef(ref: ChannelRef | SubchannelRef | ServerRef | SocketRef) {
switch (ref.kind) {
case 'channel':
delete channels[ref.id];
return;
case 'subchannel':
delete subchannels[ref.id];
return;
case 'server':
delete servers[ref.id];
return;
case 'socket':
delete sockets[ref.id];
return;
}
}
/**
* Parse a single section of an IPv6 address as two bytes
* @param addressSection A hexadecimal string of length up to 4
* @returns The pair of bytes representing this address section
*/
function parseIPv6Section(addressSection: string): [number, number] {
const numberValue = Number.parseInt(addressSection, 16);
return [numberValue / 256 | 0, numberValue % 256];
}
/**
* Parse a chunk of an IPv6 address string to some number of bytes
* @param addressChunk Some number of segments of up to 4 hexadecimal
* characters each, joined by colons.
* @returns The list of bytes representing this address chunk
*/
function parseIPv6Chunk(addressChunk: string): number[] {
if (addressChunk === '') {
return [];
}
const bytePairs = addressChunk.split(':').map(section => parseIPv6Section(section));
const result: number[] = [];
return result.concat(...bytePairs);
}
/**
* Converts an IPv4 or IPv6 address from string representation to binary
* representation
* @param ipAddress an IP address in standard IPv4 or IPv6 text format
* @returns
*/
function ipAddressStringToBuffer(ipAddress: string): Buffer | null {
if (isIPv4(ipAddress)) {
return Buffer.from(Uint8Array.from(ipAddress.split('.').map(segment => Number.parseInt(segment))));
} else if (isIPv6(ipAddress)) {
let leftSection: string;
let rightSection: string;
const doubleColonIndex = ipAddress.indexOf('::');
if (doubleColonIndex === -1) {
leftSection = ipAddress;
rightSection = '';
} else {
leftSection = ipAddress.substring(0, doubleColonIndex);
rightSection = ipAddress.substring(doubleColonIndex + 2);
}
const leftBuffer = Buffer.from(parseIPv6Chunk(leftSection));
const rightBuffer = Buffer.from(parseIPv6Chunk(rightSection));
const middleBuffer = Buffer.alloc(16 - leftBuffer.length - rightBuffer.length, 0);
return Buffer.concat([leftBuffer, middleBuffer, rightBuffer]);
} else {
return null;
}
}
function connectivityStateToMessage(state: ConnectivityState): ChannelConnectivityState__Output {
switch (state) {
case ConnectivityState.CONNECTING:
return {
state: 'CONNECTING'
};
case ConnectivityState.IDLE:
return {
state: 'IDLE'
};
case ConnectivityState.READY:
return {
state: 'READY'
};
case ConnectivityState.SHUTDOWN:
return {
state: 'SHUTDOWN'
};
case ConnectivityState.TRANSIENT_FAILURE:
return {
state: 'TRANSIENT_FAILURE'
};
default:
return {
state: 'UNKNOWN'
};
}
}
function dateToProtoTimestamp(date?: Date | null): Timestamp | null {
if (!date) {
return null;
}
const millisSinceEpoch = date.getTime();
return {
seconds: (millisSinceEpoch / 1000) | 0,
nanos: (millisSinceEpoch % 1000) * 1_000_000
}
}
function getChannelMessage(channelEntry: ChannelEntry): ChannelMessage {
const resolvedInfo = channelEntry.getInfo();
return {
ref: channelRefToMessage(channelEntry.ref),
data: {
target: resolvedInfo.target,
state: connectivityStateToMessage(resolvedInfo.state),
calls_started: resolvedInfo.callTracker.callsStarted,
calls_succeeded: resolvedInfo.callTracker.callsSucceeded,
calls_failed: resolvedInfo.callTracker.callsFailed,
last_call_started_timestamp: dateToProtoTimestamp(resolvedInfo.callTracker.lastCallStartedTimestamp),
trace: resolvedInfo.trace.getTraceMessage()
},
channel_ref: resolvedInfo.children.channels.map(ref => channelRefToMessage(ref)),
subchannel_ref: resolvedInfo.children.subchannels.map(ref => subchannelRefToMessage(ref))
};
}
function GetChannel(call: ServerUnaryCall<GetChannelRequest__Output, GetChannelResponse>, callback: sendUnaryData<GetChannelResponse>): void {
const channelId = Number.parseInt(call.request.channel_id);
const channelEntry = channels[channelId];
if (channelEntry === undefined) {
callback({
'code': Status.NOT_FOUND,
'details': 'No channel data found for id ' + channelId
});
return;
}
callback(null, {channel: getChannelMessage(channelEntry)});
}
function GetTopChannels(call: ServerUnaryCall<GetTopChannelsRequest__Output, GetTopChannelsResponse>, callback: sendUnaryData<GetTopChannelsResponse>): void {
const maxResults = Number.parseInt(call.request.max_results);
const resultList: ChannelMessage[] = [];
let i = Number.parseInt(call.request.start_channel_id);
for (; i < channels.length; i++) {
const channelEntry = channels[i];
if (channelEntry === undefined) {
continue;
}
resultList.push(getChannelMessage(channelEntry));
if (resultList.length >= maxResults) {
break;
}
}
callback(null, {
channel: resultList,
end: i >= servers.length
});
}
function getServerMessage(serverEntry: ServerEntry): ServerMessage {
const resolvedInfo = serverEntry.getInfo();
return {
ref: serverRefToMessage(serverEntry.ref),
data: {
calls_started: resolvedInfo.callTracker.callsStarted,
calls_succeeded: resolvedInfo.callTracker.callsSucceeded,
calls_failed: resolvedInfo.callTracker.callsFailed,
last_call_started_timestamp: dateToProtoTimestamp(resolvedInfo.callTracker.lastCallStartedTimestamp),
trace: resolvedInfo.trace.getTraceMessage()
},
listen_socket: resolvedInfo.listenerChildren.sockets.map(ref => socketRefToMessage(ref))
};
}
function GetServer(call: ServerUnaryCall<GetServerRequest__Output, GetServerResponse>, callback: sendUnaryData<GetServerResponse>): void {
const serverId = Number.parseInt(call.request.server_id);
const serverEntry = servers[serverId];
if (serverEntry === undefined) {
callback({
'code': Status.NOT_FOUND,
'details': 'No server data found for id ' + serverId
});
return;
}
callback(null, {server: getServerMessage(serverEntry)});
}
function GetServers(call: ServerUnaryCall<GetServersRequest__Output, GetServersResponse>, callback: sendUnaryData<GetServersResponse>): void {
const maxResults = Number.parseInt(call.request.max_results);
const resultList: ServerMessage[] = [];
let i = Number.parseInt(call.request.start_server_id);
for (; i < servers.length; i++) {
const serverEntry = servers[i];
if (serverEntry === undefined) {
continue;
}
resultList.push(getServerMessage(serverEntry));
if (resultList.length >= maxResults) {
break;
}
}
callback(null, {
server: resultList,
end: i >= servers.length
});
}
function GetSubchannel(call: ServerUnaryCall<GetSubchannelRequest__Output, GetSubchannelResponse>, callback: sendUnaryData<GetSubchannelResponse>): void {
const subchannelId = Number.parseInt(call.request.subchannel_id);
const subchannelEntry = subchannels[subchannelId];
if (subchannelEntry === undefined) {
callback({
'code': Status.NOT_FOUND,
'details': 'No subchannel data found for id ' + subchannelId
});
return;
}
const resolvedInfo = subchannelEntry.getInfo();
const subchannelMessage: SubchannelMessage = {
ref: subchannelRefToMessage(subchannelEntry.ref),
data: {
target: resolvedInfo.target,
state: connectivityStateToMessage(resolvedInfo.state),
calls_started: resolvedInfo.callTracker.callsStarted,
calls_succeeded: resolvedInfo.callTracker.callsSucceeded,
calls_failed: resolvedInfo.callTracker.callsFailed,
last_call_started_timestamp: dateToProtoTimestamp(resolvedInfo.callTracker.lastCallStartedTimestamp),
trace: resolvedInfo.trace.getTraceMessage()
},
socket_ref: resolvedInfo.children.sockets.map(ref => socketRefToMessage(ref))
};
callback(null, {subchannel: subchannelMessage});
}
function subchannelAddressToAddressMessage(subchannelAddress: SubchannelAddress): Address {
if (isTcpSubchannelAddress(subchannelAddress)) {
return {
address: 'tcpip_address',
tcpip_address: {
ip_address: ipAddressStringToBuffer(subchannelAddress.host) ?? undefined,
port: subchannelAddress.port
}
};
} else {
return {
address: 'uds_address',
uds_address: {
filename: subchannelAddress.path
}
};
}
}
function GetSocket(call: ServerUnaryCall<GetSocketRequest__Output, GetSocketResponse>, callback: sendUnaryData<GetSocketResponse>): void {
const socketId = Number.parseInt(call.request.socket_id);
const socketEntry = sockets[socketId];
if (socketEntry === undefined) {
callback({
'code': Status.NOT_FOUND,
'details': 'No socket data found for id ' + socketId
});
return;
}
const resolvedInfo = socketEntry.getInfo();
const securityMessage: Security | null = resolvedInfo.security ? {
model: 'tls',
tls: {
cipher_suite: resolvedInfo.security.cipherSuiteStandardName ? 'standard_name' : 'other_name',
standard_name: resolvedInfo.security.cipherSuiteStandardName ?? undefined,
other_name: resolvedInfo.security.cipherSuiteOtherName ?? undefined,
local_certificate: resolvedInfo.security.localCertificate ?? undefined,
remote_certificate: resolvedInfo.security.remoteCertificate ?? undefined
}
} : null;
const socketMessage: SocketMessage = {
ref: socketRefToMessage(socketEntry.ref),
local: resolvedInfo.localAddress ? subchannelAddressToAddressMessage(resolvedInfo.localAddress) : null,
remote: resolvedInfo.remoteAddress ? subchannelAddressToAddressMessage(resolvedInfo.remoteAddress) : null,
remote_name: resolvedInfo.remoteName ?? undefined,
security: securityMessage,
data: {
keep_alives_sent: resolvedInfo.keepAlivesSent,
streams_started: resolvedInfo.streamsStarted,
streams_succeeded: resolvedInfo.streamsSucceeded,
streams_failed: resolvedInfo.streamsFailed,
last_local_stream_created_timestamp: dateToProtoTimestamp(resolvedInfo.lastLocalStreamCreatedTimestamp),
last_remote_stream_created_timestamp: dateToProtoTimestamp(resolvedInfo.lastRemoteStreamCreatedTimestamp),
messages_received: resolvedInfo.messagesReceived,
messages_sent: resolvedInfo.messagesSent,
last_message_received_timestamp: dateToProtoTimestamp(resolvedInfo.lastMessageReceivedTimestamp),
last_message_sent_timestamp: dateToProtoTimestamp(resolvedInfo.lastMessageSentTimestamp),
local_flow_control_window: resolvedInfo.localFlowControlWindow ? { value: resolvedInfo.localFlowControlWindow } : null,
remote_flow_control_window: resolvedInfo.remoteFlowControlWindow ? { value: resolvedInfo.remoteFlowControlWindow } : null,
}
};
callback(null, {socket: socketMessage});
}
function GetServerSockets(call: ServerUnaryCall<GetServerSocketsRequest__Output, GetServerSocketsResponse>, callback: sendUnaryData<GetServerSocketsResponse>): void {
const serverId = Number.parseInt(call.request.server_id);
const serverEntry = servers[serverId];
if (serverEntry === undefined) {
callback({
'code': Status.NOT_FOUND,
'details': 'No server data found for id ' + serverId
});
return;
}
const startId = Number.parseInt(call.request.start_socket_id);
const maxResults = Number.parseInt(call.request.max_results);
const resolvedInfo = serverEntry.getInfo();
// If we wanted to include listener sockets in the result, this line would
// instead say
// const allSockets = resolvedInfo.listenerChildren.sockets.concat(resolvedInfo.sessionChildren.sockets).sort((ref1, ref2) => ref1.id - ref2.id);
const allSockets = resolvedInfo.sessionChildren.sockets.sort((ref1, ref2) => ref1.id - ref2.id);
const resultList: SocketRefMessage[] = [];
let i = 0;
for (; i < allSockets.length; i++) {
if (allSockets[i].id >= startId) {
resultList.push(socketRefToMessage(allSockets[i]));
if (resultList.length >= maxResults) {
break;
}
}
}
callback(null, {
socket_ref: resultList,
end: i >= allSockets.length
});
}
export function getChannelzHandlers(): ChannelzHandlers {
return {
GetChannel,
GetTopChannels,
GetServer,
GetServers,
GetSubchannel,
GetSocket,
GetServerSockets
};
}
let loadedChannelzDefinition: ChannelzDefinition | null = null;
export function getChannelzServiceDefinition(): ChannelzDefinition {
if (loadedChannelzDefinition) {
return loadedChannelzDefinition;
}
/* The purpose of this complexity is to avoid loading @grpc/proto-loader at
* runtime for users who will not use/enable channelz. */
const loaderLoadSync = require('@grpc/proto-loader').loadSync as typeof loadSync;
const loadedProto = loaderLoadSync('channelz.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: [
`${__dirname}/../../proto`
]
});
const channelzGrpcObject = loadPackageDefinition(loadedProto) as unknown as ChannelzProtoGrpcType;
loadedChannelzDefinition = channelzGrpcObject.grpc.channelz.v1.Channelz.service;
return loadedChannelzDefinition;
}
export function setup() {
registerAdminService(getChannelzServiceDefinition, getChannelzHandlers);
}
+581
View File
@@ -0,0 +1,581 @@
/*
* 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 { Metadata } from './metadata';
import {
StatusObject,
Listener,
MetadataListener,
MessageListener,
StatusListener,
FullListener,
InterceptingListener,
InterceptingListenerImpl,
isInterceptingListener,
MessageContext,
Call,
} from './call-stream';
import { Status } from './constants';
import { Channel } from './channel';
import { CallOptions } from './client';
import { CallCredentials } from './call-credentials';
import { ClientMethodDefinition } from './make-client';
/**
* Error class associated with passing both interceptors and interceptor
* providers to a client constructor or as call options.
*/
export class InterceptorConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = 'InterceptorConfigurationError';
Error.captureStackTrace(this, InterceptorConfigurationError);
}
}
export interface MetadataRequester {
(
metadata: Metadata,
listener: InterceptingListener,
next: (
metadata: Metadata,
listener: InterceptingListener | Listener
) => void
): void;
}
export interface MessageRequester {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(message: any, next: (message: any) => void): void;
}
export interface CloseRequester {
(next: () => void): void;
}
export interface CancelRequester {
(next: () => void): void;
}
/**
* An object with methods for intercepting and modifying outgoing call operations.
*/
export interface FullRequester {
start: MetadataRequester;
sendMessage: MessageRequester;
halfClose: CloseRequester;
cancel: CancelRequester;
}
export type Requester = Partial<FullRequester>;
export class ListenerBuilder {
private metadata: MetadataListener | undefined = undefined;
private message: MessageListener | undefined = undefined;
private status: StatusListener | undefined = undefined;
withOnReceiveMetadata(onReceiveMetadata: MetadataListener): this {
this.metadata = onReceiveMetadata;
return this;
}
withOnReceiveMessage(onReceiveMessage: MessageListener): this {
this.message = onReceiveMessage;
return this;
}
withOnReceiveStatus(onReceiveStatus: StatusListener): this {
this.status = onReceiveStatus;
return this;
}
build(): Listener {
return {
onReceiveMetadata: this.metadata,
onReceiveMessage: this.message,
onReceiveStatus: this.status,
};
}
}
export class RequesterBuilder {
private start: MetadataRequester | undefined = undefined;
private message: MessageRequester | undefined = undefined;
private halfClose: CloseRequester | undefined = undefined;
private cancel: CancelRequester | undefined = undefined;
withStart(start: MetadataRequester): this {
this.start = start;
return this;
}
withSendMessage(sendMessage: MessageRequester): this {
this.message = sendMessage;
return this;
}
withHalfClose(halfClose: CloseRequester): this {
this.halfClose = halfClose;
return this;
}
withCancel(cancel: CancelRequester): this {
this.cancel = cancel;
return this;
}
build(): Requester {
return {
start: this.start,
sendMessage: this.message,
halfClose: this.halfClose,
cancel: this.cancel,
};
}
}
/**
* A Listener with a default pass-through implementation of each method. Used
* for filling out Listeners with some methods omitted.
*/
const defaultListener: FullListener = {
onReceiveMetadata: (metadata, next) => {
next(metadata);
},
onReceiveMessage: (message, next) => {
next(message);
},
onReceiveStatus: (status, next) => {
next(status);
},
};
/**
* A Requester with a default pass-through implementation of each method. Used
* for filling out Requesters with some methods omitted.
*/
const defaultRequester: FullRequester = {
start: (metadata, listener, next) => {
next(metadata, listener);
},
sendMessage: (message, next) => {
next(message);
},
halfClose: (next) => {
next();
},
cancel: (next) => {
next();
},
};
export interface InterceptorOptions extends CallOptions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
method_definition: ClientMethodDefinition<any, any>;
}
export interface InterceptingCallInterface {
cancelWithStatus(status: Status, details: string): void;
getPeer(): string;
start(metadata: Metadata, listener?: Partial<InterceptingListener>): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessageWithContext(context: MessageContext, message: any): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessage(message: any): void;
startRead(): void;
halfClose(): void;
setCredentials(credentials: CallCredentials): void;
}
export class InterceptingCall implements InterceptingCallInterface {
/**
* The requester that this InterceptingCall uses to modify outgoing operations
*/
private requester: FullRequester;
/**
* Indicates that metadata has been passed to the requester's start
* method but it has not been passed to the corresponding next callback
*/
private processingMetadata = false;
/**
* Message context for a pending message that is waiting for
*/
private pendingMessageContext: MessageContext | null = null;
private pendingMessage: any;
/**
* Indicates that a message has been passed to the requester's sendMessage
* method but it has not been passed to the corresponding next callback
*/
private processingMessage = false;
/**
* Indicates that a status was received but could not be propagated because
* a message was still being processed.
*/
private pendingHalfClose = false;
constructor(
private nextCall: InterceptingCallInterface,
requester?: Requester
) {
if (requester) {
this.requester = {
start: requester.start ?? defaultRequester.start,
sendMessage: requester.sendMessage ?? defaultRequester.sendMessage,
halfClose: requester.halfClose ?? defaultRequester.halfClose,
cancel: requester.cancel ?? defaultRequester.cancel,
};
} else {
this.requester = defaultRequester;
}
}
cancelWithStatus(status: Status, details: string) {
this.requester.cancel(() => {
this.nextCall.cancelWithStatus(status, details);
});
}
getPeer() {
return this.nextCall.getPeer();
}
private processPendingMessage() {
if (this.pendingMessageContext) {
this.nextCall.sendMessageWithContext(this.pendingMessageContext, this.pendingMessage);
this.pendingMessageContext = null;
this.pendingMessage = null;
}
}
private processPendingHalfClose() {
if (this.pendingHalfClose) {
this.nextCall.halfClose();
}
}
start(
metadata: Metadata,
interceptingListener?: Partial<InterceptingListener>
): void {
const fullInterceptingListener: InterceptingListener = {
onReceiveMetadata:
interceptingListener?.onReceiveMetadata?.bind(interceptingListener) ??
((metadata) => {}),
onReceiveMessage:
interceptingListener?.onReceiveMessage?.bind(interceptingListener) ??
((message) => {}),
onReceiveStatus:
interceptingListener?.onReceiveStatus?.bind(interceptingListener) ??
((status) => {}),
};
this.processingMetadata = true;
this.requester.start(metadata, fullInterceptingListener, (md, listener) => {
this.processingMetadata = false;
let finalInterceptingListener: InterceptingListener;
if (isInterceptingListener(listener)) {
finalInterceptingListener = listener;
} else {
const fullListener: FullListener = {
onReceiveMetadata:
listener.onReceiveMetadata ?? defaultListener.onReceiveMetadata,
onReceiveMessage:
listener.onReceiveMessage ?? defaultListener.onReceiveMessage,
onReceiveStatus:
listener.onReceiveStatus ?? defaultListener.onReceiveStatus,
};
finalInterceptingListener = new InterceptingListenerImpl(
fullListener,
fullInterceptingListener
);
}
this.nextCall.start(md, finalInterceptingListener);
this.processPendingMessage();
this.processPendingHalfClose();
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessageWithContext(context: MessageContext, message: any): void {
this.processingMessage = true;
this.requester.sendMessage(message, (finalMessage) => {
this.processingMessage = false;
if (this.processingMetadata) {
this.pendingMessageContext = context;
this.pendingMessage = message;
} else {
this.nextCall.sendMessageWithContext(context, finalMessage);
this.processPendingHalfClose();
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessage(message: any): void {
this.sendMessageWithContext({}, message);
}
startRead(): void {
this.nextCall.startRead();
}
halfClose(): void {
this.requester.halfClose(() => {
if (this.processingMetadata || this.processingMessage) {
this.pendingHalfClose = true;
} else {
this.nextCall.halfClose();
}
});
}
setCredentials(credentials: CallCredentials): void {
this.nextCall.setCredentials(credentials);
}
}
function getCall(channel: Channel, path: string, options: CallOptions): Call {
const deadline = options.deadline ?? Infinity;
const host = options.host;
const parent = options.parent ?? null;
const propagateFlags = options.propagate_flags;
const credentials = options.credentials;
const call = channel.createCall(path, deadline, host, parent, propagateFlags);
if (credentials) {
call.setCredentials(credentials);
}
return call;
}
/**
* InterceptingCall implementation that directly owns the underlying Call
* object and handles serialization and deseraizliation.
*/
class BaseInterceptingCall implements InterceptingCallInterface {
constructor(
protected call: Call,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected methodDefinition: ClientMethodDefinition<any, any>
) {}
cancelWithStatus(status: Status, details: string): void {
this.call.cancelWithStatus(status, details);
}
getPeer(): string {
return this.call.getPeer();
}
setCredentials(credentials: CallCredentials): void {
this.call.setCredentials(credentials);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessageWithContext(context: MessageContext, message: any): void {
let serialized: Buffer;
try {
serialized = this.methodDefinition.requestSerialize(message);
} catch (e) {
this.call.cancelWithStatus(
Status.INTERNAL,
`Request message serialization failure: ${e.message}`
);
return;
}
this.call.sendMessageWithContext(context, serialized);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendMessage(message: any) {
this.sendMessageWithContext({}, message);
}
start(
metadata: Metadata,
interceptingListener?: Partial<InterceptingListener>
): void {
let readError: StatusObject | null = null;
this.call.start(metadata, {
onReceiveMetadata: (metadata) => {
interceptingListener?.onReceiveMetadata?.(metadata);
},
onReceiveMessage: (message) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let deserialized: any;
try {
deserialized = this.methodDefinition.responseDeserialize(message);
} catch (e) {
readError = {
code: Status.INTERNAL,
details: `Response message parsing error: ${e.message}`,
metadata: new Metadata(),
};
this.call.cancelWithStatus(readError.code, readError.details);
return;
}
interceptingListener?.onReceiveMessage?.(deserialized);
},
onReceiveStatus: (status) => {
if (readError) {
interceptingListener?.onReceiveStatus?.(readError);
} else {
interceptingListener?.onReceiveStatus?.(status);
}
},
});
}
startRead() {
this.call.startRead();
}
halfClose(): void {
this.call.halfClose();
}
}
/**
* BaseInterceptingCall with special-cased behavior for methods with unary
* responses.
*/
class BaseUnaryInterceptingCall
extends BaseInterceptingCall
implements InterceptingCallInterface {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(call: Call, methodDefinition: ClientMethodDefinition<any, any>) {
super(call, methodDefinition);
}
start(metadata: Metadata, listener?: Partial<InterceptingListener>): void {
let receivedMessage = false;
const wrapperListener: InterceptingListener = {
onReceiveMetadata:
listener?.onReceiveMetadata?.bind(listener) ?? ((metadata) => {}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onReceiveMessage: (message: any) => {
receivedMessage = true;
listener?.onReceiveMessage?.(message);
},
onReceiveStatus: (status: StatusObject) => {
if (!receivedMessage) {
listener?.onReceiveMessage?.(null);
}
listener?.onReceiveStatus?.(status);
},
};
super.start(metadata, wrapperListener);
this.call.startRead();
}
}
/**
* BaseInterceptingCall with special-cased behavior for methods with streaming
* responses.
*/
class BaseStreamingInterceptingCall
extends BaseInterceptingCall
implements InterceptingCallInterface {}
function getBottomInterceptingCall(
channel: Channel,
options: InterceptorOptions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
methodDefinition: ClientMethodDefinition<any, any>
) {
const call = getCall(channel, methodDefinition.path, options);
if (methodDefinition.responseStream) {
return new BaseStreamingInterceptingCall(call, methodDefinition);
} else {
return new BaseUnaryInterceptingCall(call, methodDefinition);
}
}
export interface NextCall {
(options: InterceptorOptions): InterceptingCallInterface;
}
export interface Interceptor {
(options: InterceptorOptions, nextCall: NextCall): InterceptingCall;
}
export interface InterceptorProvider {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(methodDefinition: ClientMethodDefinition<any, any>): Interceptor;
}
export interface InterceptorArguments {
clientInterceptors: Interceptor[];
clientInterceptorProviders: InterceptorProvider[];
callInterceptors: Interceptor[];
callInterceptorProviders: InterceptorProvider[];
}
export function getInterceptingCall(
interceptorArgs: InterceptorArguments,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
methodDefinition: ClientMethodDefinition<any, any>,
options: CallOptions,
channel: Channel
): InterceptingCallInterface {
if (
interceptorArgs.clientInterceptors.length > 0 &&
interceptorArgs.clientInterceptorProviders.length > 0
) {
throw new InterceptorConfigurationError(
'Both interceptors and interceptor_providers were passed as options ' +
'to the client constructor. Only one of these is allowed.'
);
}
if (
interceptorArgs.callInterceptors.length > 0 &&
interceptorArgs.callInterceptorProviders.length > 0
) {
throw new InterceptorConfigurationError(
'Both interceptors and interceptor_providers were passed as call ' +
'options. Only one of these is allowed.'
);
}
let interceptors: Interceptor[] = [];
// Interceptors passed to the call override interceptors passed to the client constructor
if (
interceptorArgs.callInterceptors.length > 0 ||
interceptorArgs.callInterceptorProviders.length > 0
) {
interceptors = ([] as Interceptor[])
.concat(
interceptorArgs.callInterceptors,
interceptorArgs.callInterceptorProviders.map((provider) =>
provider(methodDefinition)
)
)
.filter((interceptor) => interceptor);
// Filter out falsy values when providers return nothing
} else {
interceptors = ([] as Interceptor[])
.concat(
interceptorArgs.clientInterceptors,
interceptorArgs.clientInterceptorProviders.map((provider) =>
provider(methodDefinition)
)
)
.filter((interceptor) => interceptor);
// Filter out falsy values when providers return nothing
}
const interceptorOptions = Object.assign({}, options, {
method_definition: methodDefinition,
});
/* For each interceptor in the list, the nextCall function passed to it is
* based on the next interceptor in the list, using a nextCall function
* constructed with the following interceptor in the list, and so on. The
* initialValue, which is effectively at the end of the list, is a nextCall
* function that invokes getBottomInterceptingCall, the result of which
* handles (de)serialization and also gets the underlying call from the
* channel. */
const getCall: NextCall = interceptors.reduceRight<NextCall>(
(nextCall: NextCall, nextInterceptor: Interceptor) => {
return (currentOptions) => nextInterceptor(currentOptions, nextCall);
},
(finalOptions: InterceptorOptions) =>
getBottomInterceptingCall(channel, finalOptions, methodDefinition)
);
return getCall(interceptorOptions);
}
+680
View File
@@ -0,0 +1,680 @@
/*
* 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 {
ClientDuplexStream,
ClientDuplexStreamImpl,
ClientReadableStream,
ClientReadableStreamImpl,
ClientUnaryCall,
ClientUnaryCallImpl,
ClientWritableStream,
ClientWritableStreamImpl,
ServiceError,
callErrorFromStatus,
SurfaceCall,
} from './call';
import { CallCredentials } from './call-credentials';
import { Deadline, StatusObject } from './call-stream';
import { Channel, ChannelImplementation } from './channel';
import { ConnectivityState } from './connectivity-state';
import { ChannelCredentials } from './channel-credentials';
import { ChannelOptions } from './channel-options';
import { Status } from './constants';
import { Metadata } from './metadata';
import { ClientMethodDefinition } from './make-client';
import {
getInterceptingCall,
Interceptor,
InterceptorProvider,
InterceptorArguments,
InterceptingCallInterface,
} from './client-interceptors';
import {
ServerUnaryCall,
ServerReadableStream,
ServerWritableStream,
ServerDuplexStream,
} from './server-call';
const CHANNEL_SYMBOL = Symbol();
const INTERCEPTOR_SYMBOL = Symbol();
const INTERCEPTOR_PROVIDER_SYMBOL = Symbol();
const CALL_INVOCATION_TRANSFORMER_SYMBOL = Symbol();
function isFunction<ResponseType>(
arg: Metadata | CallOptions | UnaryCallback<ResponseType> | undefined
): arg is UnaryCallback<ResponseType> {
return typeof arg === 'function';
}
export interface UnaryCallback<ResponseType> {
(err: ServiceError | null, value?: ResponseType): void;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface CallOptions {
deadline?: Deadline;
host?: string;
parent?:
| ServerUnaryCall<any, any>
| ServerReadableStream<any, any>
| ServerWritableStream<any, any>
| ServerDuplexStream<any, any>;
propagate_flags?: number;
credentials?: CallCredentials;
interceptors?: Interceptor[];
interceptor_providers?: InterceptorProvider[];
}
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface CallProperties<RequestType, ResponseType> {
argument?: RequestType;
metadata: Metadata;
call: SurfaceCall;
channel: Channel;
methodDefinition: ClientMethodDefinition<RequestType, ResponseType>;
callOptions: CallOptions;
callback?: UnaryCallback<ResponseType>;
}
export interface CallInvocationTransformer {
(callProperties: CallProperties<any, any>): CallProperties<any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export type ClientOptions = Partial<ChannelOptions> & {
channelOverride?: Channel;
channelFactoryOverride?: (
address: string,
credentials: ChannelCredentials,
options: ClientOptions
) => Channel;
interceptors?: Interceptor[];
interceptor_providers?: InterceptorProvider[];
callInvocationTransformer?: CallInvocationTransformer;
};
/**
* A generic gRPC client. Primarily useful as a base class for all generated
* clients.
*/
export class Client {
private readonly [CHANNEL_SYMBOL]: Channel;
private readonly [INTERCEPTOR_SYMBOL]: Interceptor[];
private readonly [INTERCEPTOR_PROVIDER_SYMBOL]: InterceptorProvider[];
private readonly [CALL_INVOCATION_TRANSFORMER_SYMBOL]?: CallInvocationTransformer;
constructor(
address: string,
credentials: ChannelCredentials,
options: ClientOptions = {}
) {
options = Object.assign({}, options);
this[INTERCEPTOR_SYMBOL] = options.interceptors ?? [];
delete options.interceptors;
this[INTERCEPTOR_PROVIDER_SYMBOL] = options.interceptor_providers ?? [];
delete options.interceptor_providers;
if (
this[INTERCEPTOR_SYMBOL].length > 0 &&
this[INTERCEPTOR_PROVIDER_SYMBOL].length > 0
) {
throw new Error(
'Both interceptors and interceptor_providers were passed as options ' +
'to the client constructor. Only one of these is allowed.'
);
}
this[CALL_INVOCATION_TRANSFORMER_SYMBOL] =
options.callInvocationTransformer;
delete options.callInvocationTransformer;
if (options.channelOverride) {
this[CHANNEL_SYMBOL] = options.channelOverride;
} else if (options.channelFactoryOverride) {
const channelFactoryOverride = options.channelFactoryOverride;
delete options.channelFactoryOverride;
this[CHANNEL_SYMBOL] = channelFactoryOverride(
address,
credentials,
options
);
} else {
this[CHANNEL_SYMBOL] = new ChannelImplementation(
address,
credentials,
options
);
}
}
close(): void {
this[CHANNEL_SYMBOL].close();
}
getChannel(): Channel {
return this[CHANNEL_SYMBOL];
}
waitForReady(deadline: Deadline, callback: (error?: Error) => void): void {
const checkState = (err?: Error) => {
if (err) {
callback(new Error('Failed to connect before the deadline'));
return;
}
let newState;
try {
newState = this[CHANNEL_SYMBOL].getConnectivityState(true);
} catch (e) {
callback(new Error('The channel has been closed'));
return;
}
if (newState === ConnectivityState.READY) {
callback();
} else {
try {
this[CHANNEL_SYMBOL].watchConnectivityState(
newState,
deadline,
checkState
);
} catch (e) {
callback(new Error('The channel has been closed'));
}
}
};
setImmediate(checkState);
}
private checkOptionalUnaryResponseArguments<ResponseType>(
arg1: Metadata | CallOptions | UnaryCallback<ResponseType>,
arg2?: CallOptions | UnaryCallback<ResponseType>,
arg3?: UnaryCallback<ResponseType>
): {
metadata: Metadata;
options: CallOptions;
callback: UnaryCallback<ResponseType>;
} {
if (isFunction(arg1)) {
return { metadata: new Metadata(), options: {}, callback: arg1 };
} else if (isFunction(arg2)) {
if (arg1 instanceof Metadata) {
return { metadata: arg1, options: {}, callback: arg2 };
} else {
return { metadata: new Metadata(), options: arg1, callback: arg2 };
}
} else {
if (
!(
arg1 instanceof Metadata &&
arg2 instanceof Object &&
isFunction(arg3)
)
) {
throw new Error('Incorrect arguments passed');
}
return { metadata: arg1, options: arg2, callback: arg3 };
}
}
makeUnaryRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
metadata: Metadata,
options: CallOptions,
callback: UnaryCallback<ResponseType>
): ClientUnaryCall;
makeUnaryRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
metadata: Metadata,
callback: UnaryCallback<ResponseType>
): ClientUnaryCall;
makeUnaryRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
options: CallOptions,
callback: UnaryCallback<ResponseType>
): ClientUnaryCall;
makeUnaryRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
callback: UnaryCallback<ResponseType>
): ClientUnaryCall;
makeUnaryRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
metadata: Metadata | CallOptions | UnaryCallback<ResponseType>,
options?: CallOptions | UnaryCallback<ResponseType>,
callback?: UnaryCallback<ResponseType>
): ClientUnaryCall {
const checkedArguments = this.checkOptionalUnaryResponseArguments<ResponseType>(
metadata,
options,
callback
);
const methodDefinition: ClientMethodDefinition<
RequestType,
ResponseType
> = {
path: method,
requestStream: false,
responseStream: false,
requestSerialize: serialize,
responseDeserialize: deserialize,
};
let callProperties: CallProperties<RequestType, ResponseType> = {
argument: argument,
metadata: checkedArguments.metadata,
call: new ClientUnaryCallImpl(),
channel: this[CHANNEL_SYMBOL],
methodDefinition: methodDefinition,
callOptions: checkedArguments.options,
callback: checkedArguments.callback,
};
if (this[CALL_INVOCATION_TRANSFORMER_SYMBOL]) {
callProperties = this[CALL_INVOCATION_TRANSFORMER_SYMBOL]!(
callProperties
) as CallProperties<RequestType, ResponseType>;
}
const emitter: ClientUnaryCall = callProperties.call;
const interceptorArgs: InterceptorArguments = {
clientInterceptors: this[INTERCEPTOR_SYMBOL],
clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL],
callInterceptors: callProperties.callOptions.interceptors ?? [],
callInterceptorProviders:
callProperties.callOptions.interceptor_providers ?? [],
};
const call: InterceptingCallInterface = getInterceptingCall(
interceptorArgs,
callProperties.methodDefinition,
callProperties.callOptions,
callProperties.channel
);
/* This needs to happen before the emitter is used. Unfortunately we can't
* enforce this with the type system. We need to construct this emitter
* before calling the CallInvocationTransformer, and we need to create the
* call after that. */
emitter.call = call;
if (callProperties.callOptions.credentials) {
call.setCredentials(callProperties.callOptions.credentials);
}
let responseMessage: ResponseType | null = null;
let receivedStatus = false;
call.start(callProperties.metadata, {
onReceiveMetadata: (metadata) => {
emitter.emit('metadata', metadata);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onReceiveMessage(message: any) {
if (responseMessage !== null) {
call.cancelWithStatus(Status.INTERNAL, 'Too many responses received');
}
responseMessage = message;
},
onReceiveStatus(status: StatusObject) {
if (receivedStatus) {
return;
}
receivedStatus = true;
if (status.code === Status.OK) {
callProperties.callback!(null, responseMessage!);
} else {
callProperties.callback!(callErrorFromStatus(status));
}
emitter.emit('status', status);
},
});
call.sendMessage(argument);
call.halfClose();
return emitter;
}
makeClientStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
metadata: Metadata,
options: CallOptions,
callback: UnaryCallback<ResponseType>
): ClientWritableStream<RequestType>;
makeClientStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
metadata: Metadata,
callback: UnaryCallback<ResponseType>
): ClientWritableStream<RequestType>;
makeClientStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
options: CallOptions,
callback: UnaryCallback<ResponseType>
): ClientWritableStream<RequestType>;
makeClientStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
callback: UnaryCallback<ResponseType>
): ClientWritableStream<RequestType>;
makeClientStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
metadata: Metadata | CallOptions | UnaryCallback<ResponseType>,
options?: CallOptions | UnaryCallback<ResponseType>,
callback?: UnaryCallback<ResponseType>
): ClientWritableStream<RequestType> {
const checkedArguments = this.checkOptionalUnaryResponseArguments<ResponseType>(
metadata,
options,
callback
);
const methodDefinition: ClientMethodDefinition<
RequestType,
ResponseType
> = {
path: method,
requestStream: true,
responseStream: false,
requestSerialize: serialize,
responseDeserialize: deserialize,
};
let callProperties: CallProperties<RequestType, ResponseType> = {
metadata: checkedArguments.metadata,
call: new ClientWritableStreamImpl<RequestType>(serialize),
channel: this[CHANNEL_SYMBOL],
methodDefinition: methodDefinition,
callOptions: checkedArguments.options,
callback: checkedArguments.callback,
};
if (this[CALL_INVOCATION_TRANSFORMER_SYMBOL]) {
callProperties = this[CALL_INVOCATION_TRANSFORMER_SYMBOL]!(
callProperties
) as CallProperties<RequestType, ResponseType>;
}
const emitter: ClientWritableStream<RequestType> = callProperties.call as ClientWritableStream<RequestType>;
const interceptorArgs: InterceptorArguments = {
clientInterceptors: this[INTERCEPTOR_SYMBOL],
clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL],
callInterceptors: callProperties.callOptions.interceptors ?? [],
callInterceptorProviders:
callProperties.callOptions.interceptor_providers ?? [],
};
const call: InterceptingCallInterface = getInterceptingCall(
interceptorArgs,
callProperties.methodDefinition,
callProperties.callOptions,
callProperties.channel
);
/* This needs to happen before the emitter is used. Unfortunately we can't
* enforce this with the type system. We need to construct this emitter
* before calling the CallInvocationTransformer, and we need to create the
* call after that. */
emitter.call = call;
if (callProperties.callOptions.credentials) {
call.setCredentials(callProperties.callOptions.credentials);
}
let responseMessage: ResponseType | null = null;
let receivedStatus = false;
call.start(callProperties.metadata, {
onReceiveMetadata: (metadata) => {
emitter.emit('metadata', metadata);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onReceiveMessage(message: any) {
if (responseMessage !== null) {
call.cancelWithStatus(Status.INTERNAL, 'Too many responses received');
}
responseMessage = message;
},
onReceiveStatus(status: StatusObject) {
if (receivedStatus) {
return;
}
receivedStatus = true;
if (status.code === Status.OK) {
callProperties.callback!(null, responseMessage!);
} else {
callProperties.callback!(callErrorFromStatus(status));
}
emitter.emit('status', status);
},
});
return emitter;
}
private checkMetadataAndOptions(
arg1?: Metadata | CallOptions,
arg2?: CallOptions
): { metadata: Metadata; options: CallOptions } {
let metadata: Metadata;
let options: CallOptions;
if (arg1 instanceof Metadata) {
metadata = arg1;
if (arg2) {
options = arg2;
} else {
options = {};
}
} else {
if (arg1) {
options = arg1;
} else {
options = {};
}
metadata = new Metadata();
}
return { metadata, options };
}
makeServerStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
metadata: Metadata,
options?: CallOptions
): ClientReadableStream<ResponseType>;
makeServerStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
options?: CallOptions
): ClientReadableStream<ResponseType>;
makeServerStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
argument: RequestType,
metadata?: Metadata | CallOptions,
options?: CallOptions
): ClientReadableStream<ResponseType> {
const checkedArguments = this.checkMetadataAndOptions(metadata, options);
const methodDefinition: ClientMethodDefinition<
RequestType,
ResponseType
> = {
path: method,
requestStream: false,
responseStream: true,
requestSerialize: serialize,
responseDeserialize: deserialize,
};
let callProperties: CallProperties<RequestType, ResponseType> = {
argument: argument,
metadata: checkedArguments.metadata,
call: new ClientReadableStreamImpl<ResponseType>(deserialize),
channel: this[CHANNEL_SYMBOL],
methodDefinition: methodDefinition,
callOptions: checkedArguments.options,
};
if (this[CALL_INVOCATION_TRANSFORMER_SYMBOL]) {
callProperties = this[CALL_INVOCATION_TRANSFORMER_SYMBOL]!(
callProperties
) as CallProperties<RequestType, ResponseType>;
}
const stream: ClientReadableStream<ResponseType> = callProperties.call as ClientReadableStream<ResponseType>;
const interceptorArgs: InterceptorArguments = {
clientInterceptors: this[INTERCEPTOR_SYMBOL],
clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL],
callInterceptors: callProperties.callOptions.interceptors ?? [],
callInterceptorProviders:
callProperties.callOptions.interceptor_providers ?? [],
};
const call: InterceptingCallInterface = getInterceptingCall(
interceptorArgs,
callProperties.methodDefinition,
callProperties.callOptions,
callProperties.channel
);
/* This needs to happen before the emitter is used. Unfortunately we can't
* enforce this with the type system. We need to construct this emitter
* before calling the CallInvocationTransformer, and we need to create the
* call after that. */
stream.call = call;
if (callProperties.callOptions.credentials) {
call.setCredentials(callProperties.callOptions.credentials);
}
let receivedStatus = false;
call.start(callProperties.metadata, {
onReceiveMetadata(metadata: Metadata) {
stream.emit('metadata', metadata);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onReceiveMessage(message: any) {
stream.push(message);
},
onReceiveStatus(status: StatusObject) {
if (receivedStatus) {
return;
}
receivedStatus = true;
stream.push(null);
if (status.code !== Status.OK) {
stream.emit('error', callErrorFromStatus(status));
}
stream.emit('status', status);
},
});
call.sendMessage(argument);
call.halfClose();
return stream;
}
makeBidiStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
metadata: Metadata,
options?: CallOptions
): ClientDuplexStream<RequestType, ResponseType>;
makeBidiStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
options?: CallOptions
): ClientDuplexStream<RequestType, ResponseType>;
makeBidiStreamRequest<RequestType, ResponseType>(
method: string,
serialize: (value: RequestType) => Buffer,
deserialize: (value: Buffer) => ResponseType,
metadata?: Metadata | CallOptions,
options?: CallOptions
): ClientDuplexStream<RequestType, ResponseType> {
const checkedArguments = this.checkMetadataAndOptions(metadata, options);
const methodDefinition: ClientMethodDefinition<
RequestType,
ResponseType
> = {
path: method,
requestStream: true,
responseStream: true,
requestSerialize: serialize,
responseDeserialize: deserialize,
};
let callProperties: CallProperties<RequestType, ResponseType> = {
metadata: checkedArguments.metadata,
call: new ClientDuplexStreamImpl<RequestType, ResponseType>(
serialize,
deserialize
),
channel: this[CHANNEL_SYMBOL],
methodDefinition: methodDefinition,
callOptions: checkedArguments.options,
};
if (this[CALL_INVOCATION_TRANSFORMER_SYMBOL]) {
callProperties = this[CALL_INVOCATION_TRANSFORMER_SYMBOL]!(
callProperties
) as CallProperties<RequestType, ResponseType>;
}
const stream: ClientDuplexStream<
RequestType,
ResponseType
> = callProperties.call as ClientDuplexStream<RequestType, ResponseType>;
const interceptorArgs: InterceptorArguments = {
clientInterceptors: this[INTERCEPTOR_SYMBOL],
clientInterceptorProviders: this[INTERCEPTOR_PROVIDER_SYMBOL],
callInterceptors: callProperties.callOptions.interceptors ?? [],
callInterceptorProviders:
callProperties.callOptions.interceptor_providers ?? [],
};
const call: InterceptingCallInterface = getInterceptingCall(
interceptorArgs,
callProperties.methodDefinition,
callProperties.callOptions,
callProperties.channel
);
/* This needs to happen before the emitter is used. Unfortunately we can't
* enforce this with the type system. We need to construct this emitter
* before calling the CallInvocationTransformer, and we need to create the
* call after that. */
stream.call = call;
if (callProperties.callOptions.credentials) {
call.setCredentials(callProperties.callOptions.credentials);
}
let receivedStatus = false;
call.start(callProperties.metadata, {
onReceiveMetadata(metadata: Metadata) {
stream.emit('metadata', metadata);
},
onReceiveMessage(message: Buffer) {
stream.push(message);
},
onReceiveStatus(status: StatusObject) {
if (receivedStatus) {
return;
}
receivedStatus = true;
stream.push(null);
if (status.code !== Status.OK) {
stream.emit('error', callErrorFromStatus(status));
}
stream.emit('status', status);
},
});
return stream;
}
}
+223
View File
@@ -0,0 +1,223 @@
/*
* 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 * as zlib from 'zlib';
import { Call, WriteFlags, WriteObject } from './call-stream';
import { Channel } from './channel';
import { BaseFilter, Filter, FilterFactory } from './filter';
import { Metadata, MetadataValue } from './metadata';
abstract class CompressionHandler {
protected abstract compressMessage(message: Buffer): Promise<Buffer>;
protected abstract decompressMessage(data: Buffer): Promise<Buffer>;
/**
* @param message Raw uncompressed message bytes
* @param compress Indicates whether the message should be compressed
* @return Framed message, compressed if applicable
*/
async writeMessage(message: Buffer, compress: boolean): Promise<Buffer> {
let messageBuffer = message;
if (compress) {
messageBuffer = await this.compressMessage(messageBuffer);
}
const output = Buffer.allocUnsafe(messageBuffer.length + 5);
output.writeUInt8(compress ? 1 : 0, 0);
output.writeUInt32BE(messageBuffer.length, 1);
messageBuffer.copy(output, 5);
return output;
}
/**
* @param data Framed message, possibly compressed
* @return Uncompressed message
*/
async readMessage(data: Buffer): Promise<Buffer> {
const compressed = data.readUInt8(0) === 1;
let messageBuffer = data.slice(5);
if (compressed) {
messageBuffer = await this.decompressMessage(messageBuffer);
}
return messageBuffer;
}
}
class IdentityHandler extends CompressionHandler {
async compressMessage(message: Buffer) {
return message;
}
async writeMessage(message: Buffer, compress: boolean): Promise<Buffer> {
const output = Buffer.allocUnsafe(message.length + 5);
/* With "identity" compression, messages should always be marked as
* uncompressed */
output.writeUInt8(0, 0);
output.writeUInt32BE(message.length, 1);
message.copy(output, 5);
return output;
}
decompressMessage(message: Buffer): Promise<Buffer> {
return Promise.reject<Buffer>(
new Error(
'Received compressed message but "grpc-encoding" header was identity'
)
);
}
}
class DeflateHandler extends CompressionHandler {
compressMessage(message: Buffer) {
return new Promise<Buffer>((resolve, reject) => {
zlib.deflate(message, (err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
decompressMessage(message: Buffer) {
return new Promise<Buffer>((resolve, reject) => {
zlib.inflate(message, (err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
}
class GzipHandler extends CompressionHandler {
compressMessage(message: Buffer) {
return new Promise<Buffer>((resolve, reject) => {
zlib.gzip(message, (err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
decompressMessage(message: Buffer) {
return new Promise<Buffer>((resolve, reject) => {
zlib.unzip(message, (err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
}
class UnknownHandler extends CompressionHandler {
constructor(private readonly compressionName: string) {
super();
}
compressMessage(message: Buffer): Promise<Buffer> {
return Promise.reject<Buffer>(
new Error(
`Received message compressed with unsupported compression method ${this.compressionName}`
)
);
}
decompressMessage(message: Buffer): Promise<Buffer> {
// This should be unreachable
return Promise.reject<Buffer>(
new Error(`Compression method not supported: ${this.compressionName}`)
);
}
}
function getCompressionHandler(compressionName: string): CompressionHandler {
switch (compressionName) {
case 'identity':
return new IdentityHandler();
case 'deflate':
return new DeflateHandler();
case 'gzip':
return new GzipHandler();
default:
return new UnknownHandler(compressionName);
}
}
export class CompressionFilter extends BaseFilter implements Filter {
private sendCompression: CompressionHandler = new IdentityHandler();
private receiveCompression: CompressionHandler = new IdentityHandler();
async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
const headers: Metadata = await metadata;
headers.set('grpc-accept-encoding', 'identity,deflate,gzip');
headers.set('accept-encoding', 'identity');
return headers;
}
receiveMetadata(metadata: Metadata): Metadata {
const receiveEncoding: MetadataValue[] = metadata.get('grpc-encoding');
if (receiveEncoding.length > 0) {
const encoding: MetadataValue = receiveEncoding[0];
if (typeof encoding === 'string') {
this.receiveCompression = getCompressionHandler(encoding);
}
}
metadata.remove('grpc-encoding');
metadata.remove('grpc-accept-encoding');
return metadata;
}
async sendMessage(message: Promise<WriteObject>): Promise<WriteObject> {
/* This filter is special. The input message is the bare message bytes,
* and the output is a framed and possibly compressed message. For this
* reason, this filter should be at the bottom of the filter stack */
const resolvedMessage: WriteObject = await message;
const compress =
resolvedMessage.flags === undefined
? false
: (resolvedMessage.flags & WriteFlags.NoCompress) === 0;
return {
message: await this.sendCompression.writeMessage(
resolvedMessage.message,
compress
),
flags: resolvedMessage.flags,
};
}
async receiveMessage(message: Promise<Buffer>) {
/* This filter is also special. The input message is framed and possibly
* compressed, and the output message is deframed and uncompressed. So
* this is another reason that this filter should be at the bottom of the
* filter stack. */
return this.receiveCompression.readMessage(await message);
}
}
export class CompressionFilterFactory
implements FilterFactory<CompressionFilter> {
constructor(private readonly channel: Channel) {}
createFilter(callStream: Call): CompressionFilter {
return new CompressionFilter();
}
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2021 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.
*
*/
export enum ConnectivityState {
IDLE,
CONNECTING,
READY,
TRANSIENT_FAILURE,
SHUTDOWN,
}
+66
View File
@@ -0,0 +1,66 @@
/*
* 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.
*
*/
export enum Status {
OK = 0,
CANCELLED,
UNKNOWN,
INVALID_ARGUMENT,
DEADLINE_EXCEEDED,
NOT_FOUND,
ALREADY_EXISTS,
PERMISSION_DENIED,
RESOURCE_EXHAUSTED,
FAILED_PRECONDITION,
ABORTED,
OUT_OF_RANGE,
UNIMPLEMENTED,
INTERNAL,
UNAVAILABLE,
DATA_LOSS,
UNAUTHENTICATED,
}
export enum LogVerbosity {
DEBUG = 0,
INFO,
ERROR,
NONE,
}
/**
* NOTE: This enum is not currently used in any implemented API in this
* library. It is included only for type parity with the other implementation.
*/
export enum Propagate {
DEADLINE = 1,
CENSUS_STATS_CONTEXT = 2,
CENSUS_TRACING_CONTEXT = 4,
CANCELLATION = 8,
// https://github.com/grpc/grpc/blob/master/include/grpc/impl/codegen/propagation_bits.h#L43
DEFAULTS = 0xffff |
Propagate.DEADLINE |
Propagate.CENSUS_STATS_CONTEXT |
Propagate.CENSUS_TRACING_CONTEXT |
Propagate.CANCELLATION,
}
// -1 means unlimited
export const DEFAULT_MAX_SEND_MESSAGE_LENGTH = -1;
// 4 MB default
export const DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH = 4 * 1024 * 1024;
+120
View File
@@ -0,0 +1,120 @@
/*
* 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 { Call, StatusObject } from './call-stream';
import { Channel } from './channel';
import { Status } from './constants';
import { BaseFilter, Filter, FilterFactory } from './filter';
import { Metadata } from './metadata';
const units: Array<[string, number]> = [
['m', 1],
['S', 1000],
['M', 60 * 1000],
['H', 60 * 60 * 1000],
];
function getDeadline(deadline: number) {
const now = new Date().getTime();
const timeoutMs = Math.max(deadline - now, 0);
for (const [unit, factor] of units) {
const amount = timeoutMs / factor;
if (amount < 1e8) {
return String(Math.ceil(amount)) + unit;
}
}
throw new Error('Deadline is too far in the future');
}
export class DeadlineFilter extends BaseFilter implements Filter {
private timer: NodeJS.Timer | null = null;
private deadline = Infinity;
constructor(
private readonly channel: Channel,
private readonly callStream: Call
) {
super();
this.retreiveDeadline();
this.runTimer();
}
private retreiveDeadline() {
const callDeadline = this.callStream.getDeadline();
if (callDeadline instanceof Date) {
this.deadline = callDeadline.getTime();
} else {
this.deadline = callDeadline;
}
}
private runTimer() {
if (this.timer) {
clearTimeout(this.timer);
}
const now: number = new Date().getTime();
const timeout = this.deadline - now;
if (timeout <= 0) {
process.nextTick(() => {
this.callStream.cancelWithStatus(
Status.DEADLINE_EXCEEDED,
'Deadline exceeded'
);
});
} else if (this.deadline !== Infinity) {
this.timer = setTimeout(() => {
this.callStream.cancelWithStatus(
Status.DEADLINE_EXCEEDED,
'Deadline exceeded'
);
}, timeout);
this.timer.unref?.();
}
}
refresh() {
this.retreiveDeadline();
this.runTimer();
}
async sendMetadata(metadata: Promise<Metadata>) {
if (this.deadline === Infinity) {
return metadata;
}
/* The input metadata promise depends on the original channel.connect()
* promise, so when it is complete that implies that the channel is
* connected */
const finalMetadata = await metadata;
const timeoutString = getDeadline(this.deadline);
finalMetadata.set('grpc-timeout', timeoutString);
return finalMetadata;
}
receiveTrailers(status: StatusObject) {
if (this.timer) {
clearTimeout(this.timer);
}
return status;
}
}
export class DeadlineFilterFactory implements FilterFactory<DeadlineFilter> {
constructor(private readonly channel: Channel) {}
createFilter(callStream: Call): DeadlineFilter {
return new DeadlineFilter(this.channel, callStream);
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* 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.
*
*/
export interface EmitterAugmentation1<Name extends string | symbol, Arg> {
addListener(event: Name, listener: (arg1: Arg) => void): this;
emit(event: Name, arg1: Arg): boolean;
on(event: Name, listener: (arg1: Arg) => void): this;
once(event: Name, listener: (arg1: Arg) => void): this;
prependListener(event: Name, listener: (arg1: Arg) => void): this;
prependOnceListener(event: Name, listener: (arg1: Arg) => void): this;
removeListener(event: Name, listener: (arg1: Arg) => void): this;
}
+36
View File
@@ -0,0 +1,36 @@
export { trace } from './logging';
export {
Resolver,
ResolverListener,
registerResolver,
ConfigSelector,
} from './resolver';
export { GrpcUri, uriToString } from './uri-parser';
export { ServiceConfig, Duration } from './service-config';
export { BackoffTimeout } from './backoff-timeout';
export {
LoadBalancer,
LoadBalancingConfig,
ChannelControlHelper,
createChildChannelControlHelper,
registerLoadBalancerType,
getFirstUsableConfig,
validateLoadBalancingConfig,
} from './load-balancer';
export {
SubchannelAddress,
subchannelAddressToString,
} from './subchannel-address';
export { ChildLoadBalancerHandler } from './load-balancer-child-handler';
export {
Picker,
UnavailablePicker,
QueuePicker,
PickResult,
PickArgs,
PickResultType,
} from './picker';
export { Call as CallStream } from './call-stream';
export { Filter, BaseFilter, FilterFactory } from './filter';
export { FilterStackFactory } from './filter-stack';
export { registerAdminService } from './admin';
+102
View File
@@ -0,0 +1,102 @@
/*
* 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 { Call, StatusObject, WriteObject } from './call-stream';
import { Filter, FilterFactory } from './filter';
import { Metadata } from './metadata';
export class FilterStack implements Filter {
constructor(private readonly filters: Filter[]) {}
sendMetadata(metadata: Promise<Metadata>) {
let result: Promise<Metadata> = metadata;
for (let i = 0; i < this.filters.length; i++) {
result = this.filters[i].sendMetadata(result);
}
return result;
}
receiveMetadata(metadata: Metadata) {
let result: Metadata = metadata;
for (let i = this.filters.length - 1; i >= 0; i--) {
result = this.filters[i].receiveMetadata(result);
}
return result;
}
sendMessage(message: Promise<WriteObject>): Promise<WriteObject> {
let result: Promise<WriteObject> = message;
for (let i = 0; i < this.filters.length; i++) {
result = this.filters[i].sendMessage(result);
}
return result;
}
receiveMessage(message: Promise<Buffer>): Promise<Buffer> {
let result: Promise<Buffer> = message;
for (let i = this.filters.length - 1; i >= 0; i--) {
result = this.filters[i].receiveMessage(result);
}
return result;
}
receiveTrailers(status: StatusObject): StatusObject {
let result: StatusObject = status;
for (let i = this.filters.length - 1; i >= 0; i--) {
result = this.filters[i].receiveTrailers(result);
}
return result;
}
refresh(): void {
for (const filter of this.filters) {
filter.refresh();
}
}
push(filters: Filter[]) {
this.filters.unshift(...filters);
}
getFilters(): Filter[] {
return this.filters;
}
}
export class FilterStackFactory implements FilterFactory<FilterStack> {
constructor(private readonly factories: Array<FilterFactory<Filter>>) {}
push(filterFactories: FilterFactory<Filter>[]) {
this.factories.unshift(...filterFactories);
}
createFilter(callStream: Call): FilterStack {
return new FilterStack(
this.factories.map((factory) => factory.createFilter(callStream))
);
}
}
+65
View File
@@ -0,0 +1,65 @@
/*
* 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 { Call, StatusObject, WriteObject } from './call-stream';
import { Metadata } from './metadata';
/**
* Filter classes represent related per-call logic and state that is primarily
* used to modify incoming and outgoing data
*/
export interface Filter {
sendMetadata(metadata: Promise<Metadata>): Promise<Metadata>;
receiveMetadata(metadata: Metadata): Metadata;
sendMessage(message: Promise<WriteObject>): Promise<WriteObject>;
receiveMessage(message: Promise<Buffer>): Promise<Buffer>;
receiveTrailers(status: StatusObject): StatusObject;
refresh(): void;
}
export abstract class BaseFilter implements Filter {
async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
return metadata;
}
receiveMetadata(metadata: Metadata): Metadata {
return metadata;
}
async sendMessage(message: Promise<WriteObject>): Promise<WriteObject> {
return message;
}
async receiveMessage(message: Promise<Buffer>): Promise<Buffer> {
return message;
}
receiveTrailers(status: StatusObject): StatusObject {
return status;
}
refresh(): void {}
}
export interface FilterFactory<T extends Filter> {
createFilter(callStream: Call): T;
}
+73
View File
@@ -0,0 +1,73 @@
import type * as grpc from '../index';
import type { MessageTypeDefinition } from '@grpc/proto-loader';
import type { ChannelzClient as _grpc_channelz_v1_ChannelzClient, ChannelzDefinition as _grpc_channelz_v1_ChannelzDefinition } from './grpc/channelz/v1/Channelz';
type SubtypeConstructor<Constructor extends new (...args: any) => any, Subtype> = {
new(...args: ConstructorParameters<Constructor>): Subtype;
};
export interface ProtoGrpcType {
google: {
protobuf: {
Any: MessageTypeDefinition
BoolValue: MessageTypeDefinition
BytesValue: MessageTypeDefinition
DoubleValue: MessageTypeDefinition
Duration: MessageTypeDefinition
FloatValue: MessageTypeDefinition
Int32Value: MessageTypeDefinition
Int64Value: MessageTypeDefinition
StringValue: MessageTypeDefinition
Timestamp: MessageTypeDefinition
UInt32Value: MessageTypeDefinition
UInt64Value: MessageTypeDefinition
}
}
grpc: {
channelz: {
v1: {
Address: MessageTypeDefinition
Channel: MessageTypeDefinition
ChannelConnectivityState: MessageTypeDefinition
ChannelData: MessageTypeDefinition
ChannelRef: MessageTypeDefinition
ChannelTrace: MessageTypeDefinition
ChannelTraceEvent: MessageTypeDefinition
/**
* Channelz is a service exposed by gRPC servers that provides detailed debug
* information.
*/
Channelz: SubtypeConstructor<typeof grpc.Client, _grpc_channelz_v1_ChannelzClient> & { service: _grpc_channelz_v1_ChannelzDefinition }
GetChannelRequest: MessageTypeDefinition
GetChannelResponse: MessageTypeDefinition
GetServerRequest: MessageTypeDefinition
GetServerResponse: MessageTypeDefinition
GetServerSocketsRequest: MessageTypeDefinition
GetServerSocketsResponse: MessageTypeDefinition
GetServersRequest: MessageTypeDefinition
GetServersResponse: MessageTypeDefinition
GetSocketRequest: MessageTypeDefinition
GetSocketResponse: MessageTypeDefinition
GetSubchannelRequest: MessageTypeDefinition
GetSubchannelResponse: MessageTypeDefinition
GetTopChannelsRequest: MessageTypeDefinition
GetTopChannelsResponse: MessageTypeDefinition
Security: MessageTypeDefinition
Server: MessageTypeDefinition
ServerData: MessageTypeDefinition
ServerRef: MessageTypeDefinition
Socket: MessageTypeDefinition
SocketData: MessageTypeDefinition
SocketOption: MessageTypeDefinition
SocketOptionLinger: MessageTypeDefinition
SocketOptionTcpInfo: MessageTypeDefinition
SocketOptionTimeout: MessageTypeDefinition
SocketRef: MessageTypeDefinition
Subchannel: MessageTypeDefinition
SubchannelRef: MessageTypeDefinition
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
// Original file: null
import type { AnyExtension } from '@grpc/proto-loader';
export type Any = AnyExtension | {
type_url: string;
value: Buffer | Uint8Array | string;
}
export interface Any__Output {
'type_url': (string);
'value': (Buffer);
}
+10
View File
@@ -0,0 +1,10 @@
// Original file: null
export interface BoolValue {
'value'?: (boolean);
}
export interface BoolValue__Output {
'value': (boolean);
}
+10
View File
@@ -0,0 +1,10 @@
// Original file: null
export interface BytesValue {
'value'?: (Buffer | Uint8Array | string);
}
export interface BytesValue__Output {
'value': (Buffer);
}
@@ -0,0 +1,10 @@
// Original file: null
export interface DoubleValue {
'value'?: (number | string);
}
export interface DoubleValue__Output {
'value': (number);
}
+13
View File
@@ -0,0 +1,13 @@
// Original file: null
import type { Long } from '@grpc/proto-loader';
export interface Duration {
'seconds'?: (number | string | Long);
'nanos'?: (number);
}
export interface Duration__Output {
'seconds': (string);
'nanos': (number);
}
+10
View File
@@ -0,0 +1,10 @@
// Original file: null
export interface FloatValue {
'value'?: (number | string);
}
export interface FloatValue__Output {
'value': (number);
}
+10
View File
@@ -0,0 +1,10 @@
// Original file: null
export interface Int32Value {
'value'?: (number);
}
export interface Int32Value__Output {
'value': (number);
}
+11
View File
@@ -0,0 +1,11 @@
// Original file: null
import type { Long } from '@grpc/proto-loader';
export interface Int64Value {
'value'?: (number | string | Long);
}
export interface Int64Value__Output {
'value': (string);
}
@@ -0,0 +1,10 @@
// Original file: null
export interface StringValue {
'value'?: (string);
}
export interface StringValue__Output {
'value': (string);
}
+13
View File
@@ -0,0 +1,13 @@
// Original file: null
import type { Long } from '@grpc/proto-loader';
export interface Timestamp {
'seconds'?: (number | string | Long);
'nanos'?: (number);
}
export interface Timestamp__Output {
'seconds': (string);
'nanos': (number);
}
@@ -0,0 +1,10 @@
// Original file: null
export interface UInt32Value {
'value'?: (number);
}
export interface UInt32Value__Output {
'value': (number);
}
@@ -0,0 +1,11 @@
// Original file: null
import type { Long } from '@grpc/proto-loader';
export interface UInt64Value {
'value'?: (number | string | Long);
}
export interface UInt64Value__Output {
'value': (string);
}
+89
View File
@@ -0,0 +1,89 @@
// Original file: proto/channelz.proto
import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any';
/**
* An address type not included above.
*/
export interface _grpc_channelz_v1_Address_OtherAddress {
/**
* The human readable version of the value. This value should be set.
*/
'name'?: (string);
/**
* The actual address message.
*/
'value'?: (_google_protobuf_Any | null);
}
/**
* An address type not included above.
*/
export interface _grpc_channelz_v1_Address_OtherAddress__Output {
/**
* The human readable version of the value. This value should be set.
*/
'name': (string);
/**
* The actual address message.
*/
'value': (_google_protobuf_Any__Output | null);
}
export interface _grpc_channelz_v1_Address_TcpIpAddress {
/**
* Either the IPv4 or IPv6 address in bytes. Will be either 4 bytes or 16
* bytes in length.
*/
'ip_address'?: (Buffer | Uint8Array | string);
/**
* 0-64k, or -1 if not appropriate.
*/
'port'?: (number);
}
export interface _grpc_channelz_v1_Address_TcpIpAddress__Output {
/**
* Either the IPv4 or IPv6 address in bytes. Will be either 4 bytes or 16
* bytes in length.
*/
'ip_address': (Buffer);
/**
* 0-64k, or -1 if not appropriate.
*/
'port': (number);
}
/**
* A Unix Domain Socket address.
*/
export interface _grpc_channelz_v1_Address_UdsAddress {
'filename'?: (string);
}
/**
* A Unix Domain Socket address.
*/
export interface _grpc_channelz_v1_Address_UdsAddress__Output {
'filename': (string);
}
/**
* Address represents the address used to create the socket.
*/
export interface Address {
'tcpip_address'?: (_grpc_channelz_v1_Address_TcpIpAddress | null);
'uds_address'?: (_grpc_channelz_v1_Address_UdsAddress | null);
'other_address'?: (_grpc_channelz_v1_Address_OtherAddress | null);
'address'?: "tcpip_address"|"uds_address"|"other_address";
}
/**
* Address represents the address used to create the socket.
*/
export interface Address__Output {
'tcpip_address'?: (_grpc_channelz_v1_Address_TcpIpAddress__Output | null);
'uds_address'?: (_grpc_channelz_v1_Address_UdsAddress__Output | null);
'other_address'?: (_grpc_channelz_v1_Address_OtherAddress__Output | null);
'address': "tcpip_address"|"uds_address"|"other_address";
}
+68
View File
@@ -0,0 +1,68 @@
// Original file: proto/channelz.proto
import type { ChannelRef as _grpc_channelz_v1_ChannelRef, ChannelRef__Output as _grpc_channelz_v1_ChannelRef__Output } from '../../../grpc/channelz/v1/ChannelRef';
import type { ChannelData as _grpc_channelz_v1_ChannelData, ChannelData__Output as _grpc_channelz_v1_ChannelData__Output } from '../../../grpc/channelz/v1/ChannelData';
import type { SubchannelRef as _grpc_channelz_v1_SubchannelRef, SubchannelRef__Output as _grpc_channelz_v1_SubchannelRef__Output } from '../../../grpc/channelz/v1/SubchannelRef';
import type { SocketRef as _grpc_channelz_v1_SocketRef, SocketRef__Output as _grpc_channelz_v1_SocketRef__Output } from '../../../grpc/channelz/v1/SocketRef';
/**
* Channel is a logical grouping of channels, subchannels, and sockets.
*/
export interface Channel {
/**
* The identifier for this channel. This should bet set.
*/
'ref'?: (_grpc_channelz_v1_ChannelRef | null);
/**
* Data specific to this channel.
*/
'data'?: (_grpc_channelz_v1_ChannelData | null);
/**
* There are no ordering guarantees on the order of channel refs.
* There may not be cycles in the ref graph.
* A channel ref may be present in more than one channel or subchannel.
*/
'channel_ref'?: (_grpc_channelz_v1_ChannelRef)[];
/**
* At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
* There are no ordering guarantees on the order of subchannel refs.
* There may not be cycles in the ref graph.
* A sub channel ref may be present in more than one channel or subchannel.
*/
'subchannel_ref'?: (_grpc_channelz_v1_SubchannelRef)[];
/**
* There are no ordering guarantees on the order of sockets.
*/
'socket_ref'?: (_grpc_channelz_v1_SocketRef)[];
}
/**
* Channel is a logical grouping of channels, subchannels, and sockets.
*/
export interface Channel__Output {
/**
* The identifier for this channel. This should bet set.
*/
'ref': (_grpc_channelz_v1_ChannelRef__Output | null);
/**
* Data specific to this channel.
*/
'data': (_grpc_channelz_v1_ChannelData__Output | null);
/**
* There are no ordering guarantees on the order of channel refs.
* There may not be cycles in the ref graph.
* A channel ref may be present in more than one channel or subchannel.
*/
'channel_ref': (_grpc_channelz_v1_ChannelRef__Output)[];
/**
* At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
* There are no ordering guarantees on the order of subchannel refs.
* There may not be cycles in the ref graph.
* A sub channel ref may be present in more than one channel or subchannel.
*/
'subchannel_ref': (_grpc_channelz_v1_SubchannelRef__Output)[];
/**
* There are no ordering guarantees on the order of sockets.
*/
'socket_ref': (_grpc_channelz_v1_SocketRef__Output)[];
}
@@ -0,0 +1,29 @@
// Original file: proto/channelz.proto
// Original file: proto/channelz.proto
export enum _grpc_channelz_v1_ChannelConnectivityState_State {
UNKNOWN = 0,
IDLE = 1,
CONNECTING = 2,
READY = 3,
TRANSIENT_FAILURE = 4,
SHUTDOWN = 5,
}
/**
* These come from the specified states in this document:
* https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md
*/
export interface ChannelConnectivityState {
'state'?: (_grpc_channelz_v1_ChannelConnectivityState_State | keyof typeof _grpc_channelz_v1_ChannelConnectivityState_State);
}
/**
* These come from the specified states in this document:
* https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md
*/
export interface ChannelConnectivityState__Output {
'state': (keyof typeof _grpc_channelz_v1_ChannelConnectivityState_State);
}
@@ -0,0 +1,76 @@
// Original file: proto/channelz.proto
import type { ChannelConnectivityState as _grpc_channelz_v1_ChannelConnectivityState, ChannelConnectivityState__Output as _grpc_channelz_v1_ChannelConnectivityState__Output } from '../../../grpc/channelz/v1/ChannelConnectivityState';
import type { ChannelTrace as _grpc_channelz_v1_ChannelTrace, ChannelTrace__Output as _grpc_channelz_v1_ChannelTrace__Output } from '../../../grpc/channelz/v1/ChannelTrace';
import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp';
import type { Long } from '@grpc/proto-loader';
/**
* Channel data is data related to a specific Channel or Subchannel.
*/
export interface ChannelData {
/**
* The connectivity state of the channel or subchannel. Implementations
* should always set this.
*/
'state'?: (_grpc_channelz_v1_ChannelConnectivityState | null);
/**
* The target this channel originally tried to connect to. May be absent
*/
'target'?: (string);
/**
* A trace of recent events on the channel. May be absent.
*/
'trace'?: (_grpc_channelz_v1_ChannelTrace | null);
/**
* The number of calls started on the channel
*/
'calls_started'?: (number | string | Long);
/**
* The number of calls that have completed with an OK status
*/
'calls_succeeded'?: (number | string | Long);
/**
* The number of calls that have completed with a non-OK status
*/
'calls_failed'?: (number | string | Long);
/**
* The last time a call was started on the channel.
*/
'last_call_started_timestamp'?: (_google_protobuf_Timestamp | null);
}
/**
* Channel data is data related to a specific Channel or Subchannel.
*/
export interface ChannelData__Output {
/**
* The connectivity state of the channel or subchannel. Implementations
* should always set this.
*/
'state': (_grpc_channelz_v1_ChannelConnectivityState__Output | null);
/**
* The target this channel originally tried to connect to. May be absent
*/
'target': (string);
/**
* A trace of recent events on the channel. May be absent.
*/
'trace': (_grpc_channelz_v1_ChannelTrace__Output | null);
/**
* The number of calls started on the channel
*/
'calls_started': (string);
/**
* The number of calls that have completed with an OK status
*/
'calls_succeeded': (string);
/**
* The number of calls that have completed with a non-OK status
*/
'calls_failed': (string);
/**
* The last time a call was started on the channel.
*/
'last_call_started_timestamp': (_google_protobuf_Timestamp__Output | null);
}
@@ -0,0 +1,31 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
/**
* ChannelRef is a reference to a Channel.
*/
export interface ChannelRef {
/**
* The globally unique id for this channel. Must be a positive number.
*/
'channel_id'?: (number | string | Long);
/**
* An optional name associated with the channel.
*/
'name'?: (string);
}
/**
* ChannelRef is a reference to a Channel.
*/
export interface ChannelRef__Output {
/**
* The globally unique id for this channel. Must be a positive number.
*/
'channel_id': (string);
/**
* An optional name associated with the channel.
*/
'name': (string);
}
@@ -0,0 +1,45 @@
// Original file: proto/channelz.proto
import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp';
import type { ChannelTraceEvent as _grpc_channelz_v1_ChannelTraceEvent, ChannelTraceEvent__Output as _grpc_channelz_v1_ChannelTraceEvent__Output } from '../../../grpc/channelz/v1/ChannelTraceEvent';
import type { Long } from '@grpc/proto-loader';
/**
* ChannelTrace represents the recent events that have occurred on the channel.
*/
export interface ChannelTrace {
/**
* Number of events ever logged in this tracing object. This can differ from
* events.size() because events can be overwritten or garbage collected by
* implementations.
*/
'num_events_logged'?: (number | string | Long);
/**
* Time that this channel was created.
*/
'creation_timestamp'?: (_google_protobuf_Timestamp | null);
/**
* List of events that have occurred on this channel.
*/
'events'?: (_grpc_channelz_v1_ChannelTraceEvent)[];
}
/**
* ChannelTrace represents the recent events that have occurred on the channel.
*/
export interface ChannelTrace__Output {
/**
* Number of events ever logged in this tracing object. This can differ from
* events.size() because events can be overwritten or garbage collected by
* implementations.
*/
'num_events_logged': (string);
/**
* Time that this channel was created.
*/
'creation_timestamp': (_google_protobuf_Timestamp__Output | null);
/**
* List of events that have occurred on this channel.
*/
'events': (_grpc_channelz_v1_ChannelTraceEvent__Output)[];
}
@@ -0,0 +1,73 @@
// Original file: proto/channelz.proto
import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp';
import type { ChannelRef as _grpc_channelz_v1_ChannelRef, ChannelRef__Output as _grpc_channelz_v1_ChannelRef__Output } from '../../../grpc/channelz/v1/ChannelRef';
import type { SubchannelRef as _grpc_channelz_v1_SubchannelRef, SubchannelRef__Output as _grpc_channelz_v1_SubchannelRef__Output } from '../../../grpc/channelz/v1/SubchannelRef';
// Original file: proto/channelz.proto
/**
* The supported severity levels of trace events.
*/
export enum _grpc_channelz_v1_ChannelTraceEvent_Severity {
CT_UNKNOWN = 0,
CT_INFO = 1,
CT_WARNING = 2,
CT_ERROR = 3,
}
/**
* A trace event is an interesting thing that happened to a channel or
* subchannel, such as creation, address resolution, subchannel creation, etc.
*/
export interface ChannelTraceEvent {
/**
* High level description of the event.
*/
'description'?: (string);
/**
* the severity of the trace event
*/
'severity'?: (_grpc_channelz_v1_ChannelTraceEvent_Severity | keyof typeof _grpc_channelz_v1_ChannelTraceEvent_Severity);
/**
* When this event occurred.
*/
'timestamp'?: (_google_protobuf_Timestamp | null);
'channel_ref'?: (_grpc_channelz_v1_ChannelRef | null);
'subchannel_ref'?: (_grpc_channelz_v1_SubchannelRef | null);
/**
* ref of referenced channel or subchannel.
* Optional, only present if this event refers to a child object. For example,
* this field would be filled if this trace event was for a subchannel being
* created.
*/
'child_ref'?: "channel_ref"|"subchannel_ref";
}
/**
* A trace event is an interesting thing that happened to a channel or
* subchannel, such as creation, address resolution, subchannel creation, etc.
*/
export interface ChannelTraceEvent__Output {
/**
* High level description of the event.
*/
'description': (string);
/**
* the severity of the trace event
*/
'severity': (keyof typeof _grpc_channelz_v1_ChannelTraceEvent_Severity);
/**
* When this event occurred.
*/
'timestamp': (_google_protobuf_Timestamp__Output | null);
'channel_ref'?: (_grpc_channelz_v1_ChannelRef__Output | null);
'subchannel_ref'?: (_grpc_channelz_v1_SubchannelRef__Output | null);
/**
* ref of referenced channel or subchannel.
* Optional, only present if this event refers to a child object. For example,
* this field would be filled if this trace event was for a subchannel being
* created.
*/
'child_ref': "channel_ref"|"subchannel_ref";
}
+178
View File
@@ -0,0 +1,178 @@
// Original file: proto/channelz.proto
import type * as grpc from '../../../../index'
import type { MethodDefinition } from '@grpc/proto-loader'
import type { GetChannelRequest as _grpc_channelz_v1_GetChannelRequest, GetChannelRequest__Output as _grpc_channelz_v1_GetChannelRequest__Output } from '../../../grpc/channelz/v1/GetChannelRequest';
import type { GetChannelResponse as _grpc_channelz_v1_GetChannelResponse, GetChannelResponse__Output as _grpc_channelz_v1_GetChannelResponse__Output } from '../../../grpc/channelz/v1/GetChannelResponse';
import type { GetServerRequest as _grpc_channelz_v1_GetServerRequest, GetServerRequest__Output as _grpc_channelz_v1_GetServerRequest__Output } from '../../../grpc/channelz/v1/GetServerRequest';
import type { GetServerResponse as _grpc_channelz_v1_GetServerResponse, GetServerResponse__Output as _grpc_channelz_v1_GetServerResponse__Output } from '../../../grpc/channelz/v1/GetServerResponse';
import type { GetServerSocketsRequest as _grpc_channelz_v1_GetServerSocketsRequest, GetServerSocketsRequest__Output as _grpc_channelz_v1_GetServerSocketsRequest__Output } from '../../../grpc/channelz/v1/GetServerSocketsRequest';
import type { GetServerSocketsResponse as _grpc_channelz_v1_GetServerSocketsResponse, GetServerSocketsResponse__Output as _grpc_channelz_v1_GetServerSocketsResponse__Output } from '../../../grpc/channelz/v1/GetServerSocketsResponse';
import type { GetServersRequest as _grpc_channelz_v1_GetServersRequest, GetServersRequest__Output as _grpc_channelz_v1_GetServersRequest__Output } from '../../../grpc/channelz/v1/GetServersRequest';
import type { GetServersResponse as _grpc_channelz_v1_GetServersResponse, GetServersResponse__Output as _grpc_channelz_v1_GetServersResponse__Output } from '../../../grpc/channelz/v1/GetServersResponse';
import type { GetSocketRequest as _grpc_channelz_v1_GetSocketRequest, GetSocketRequest__Output as _grpc_channelz_v1_GetSocketRequest__Output } from '../../../grpc/channelz/v1/GetSocketRequest';
import type { GetSocketResponse as _grpc_channelz_v1_GetSocketResponse, GetSocketResponse__Output as _grpc_channelz_v1_GetSocketResponse__Output } from '../../../grpc/channelz/v1/GetSocketResponse';
import type { GetSubchannelRequest as _grpc_channelz_v1_GetSubchannelRequest, GetSubchannelRequest__Output as _grpc_channelz_v1_GetSubchannelRequest__Output } from '../../../grpc/channelz/v1/GetSubchannelRequest';
import type { GetSubchannelResponse as _grpc_channelz_v1_GetSubchannelResponse, GetSubchannelResponse__Output as _grpc_channelz_v1_GetSubchannelResponse__Output } from '../../../grpc/channelz/v1/GetSubchannelResponse';
import type { GetTopChannelsRequest as _grpc_channelz_v1_GetTopChannelsRequest, GetTopChannelsRequest__Output as _grpc_channelz_v1_GetTopChannelsRequest__Output } from '../../../grpc/channelz/v1/GetTopChannelsRequest';
import type { GetTopChannelsResponse as _grpc_channelz_v1_GetTopChannelsResponse, GetTopChannelsResponse__Output as _grpc_channelz_v1_GetTopChannelsResponse__Output } from '../../../grpc/channelz/v1/GetTopChannelsResponse';
/**
* Channelz is a service exposed by gRPC servers that provides detailed debug
* information.
*/
export interface ChannelzClient extends grpc.Client {
/**
* Returns a single Channel, or else a NOT_FOUND code.
*/
GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall;
GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall;
GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall;
GetChannel(argument: _grpc_channelz_v1_GetChannelRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetChannelResponse__Output>): grpc.ClientUnaryCall;
/**
* Returns a single Server, or else a NOT_FOUND code.
*/
GetServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
GetServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
GetServer(argument: _grpc_channelz_v1_GetServerRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
GetServer(argument: _grpc_channelz_v1_GetServerRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
/**
* Returns a single Server, or else a NOT_FOUND code.
*/
getServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
getServer(argument: _grpc_channelz_v1_GetServerRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
getServer(argument: _grpc_channelz_v1_GetServerRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
getServer(argument: _grpc_channelz_v1_GetServerRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerResponse__Output>): grpc.ClientUnaryCall;
/**
* Gets all server sockets that exist in the process.
*/
GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
GetServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
/**
* Gets all server sockets that exist in the process.
*/
getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
getServerSockets(argument: _grpc_channelz_v1_GetServerSocketsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServerSocketsResponse__Output>): grpc.ClientUnaryCall;
/**
* Gets all servers that exist in the process.
*/
GetServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
GetServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
GetServers(argument: _grpc_channelz_v1_GetServersRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
GetServers(argument: _grpc_channelz_v1_GetServersRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
/**
* Gets all servers that exist in the process.
*/
getServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
getServers(argument: _grpc_channelz_v1_GetServersRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
getServers(argument: _grpc_channelz_v1_GetServersRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
getServers(argument: _grpc_channelz_v1_GetServersRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetServersResponse__Output>): grpc.ClientUnaryCall;
/**
* Returns a single Socket or else a NOT_FOUND code.
*/
GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
GetSocket(argument: _grpc_channelz_v1_GetSocketRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
/**
* Returns a single Socket or else a NOT_FOUND code.
*/
getSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
getSocket(argument: _grpc_channelz_v1_GetSocketRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
getSocket(argument: _grpc_channelz_v1_GetSocketRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
getSocket(argument: _grpc_channelz_v1_GetSocketRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSocketResponse__Output>): grpc.ClientUnaryCall;
/**
* Returns a single Subchannel, or else a NOT_FOUND code.
*/
GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
GetSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
/**
* Returns a single Subchannel, or else a NOT_FOUND code.
*/
getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
getSubchannel(argument: _grpc_channelz_v1_GetSubchannelRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetSubchannelResponse__Output>): grpc.ClientUnaryCall;
/**
* Gets all root channels (i.e. channels the application has directly
* created). This does not include subchannels nor non-top level channels.
*/
GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
GetTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
/**
* Gets all root channels (i.e. channels the application has directly
* created). This does not include subchannels nor non-top level channels.
*/
getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, metadata: grpc.Metadata, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, options: grpc.CallOptions, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
getTopChannels(argument: _grpc_channelz_v1_GetTopChannelsRequest, callback: grpc.requestCallback<_grpc_channelz_v1_GetTopChannelsResponse__Output>): grpc.ClientUnaryCall;
}
/**
* Channelz is a service exposed by gRPC servers that provides detailed debug
* information.
*/
export interface ChannelzHandlers extends grpc.UntypedServiceImplementation {
/**
* Returns a single Channel, or else a NOT_FOUND code.
*/
GetChannel: grpc.handleUnaryCall<_grpc_channelz_v1_GetChannelRequest__Output, _grpc_channelz_v1_GetChannelResponse>;
/**
* Returns a single Server, or else a NOT_FOUND code.
*/
GetServer: grpc.handleUnaryCall<_grpc_channelz_v1_GetServerRequest__Output, _grpc_channelz_v1_GetServerResponse>;
/**
* Gets all server sockets that exist in the process.
*/
GetServerSockets: grpc.handleUnaryCall<_grpc_channelz_v1_GetServerSocketsRequest__Output, _grpc_channelz_v1_GetServerSocketsResponse>;
/**
* Gets all servers that exist in the process.
*/
GetServers: grpc.handleUnaryCall<_grpc_channelz_v1_GetServersRequest__Output, _grpc_channelz_v1_GetServersResponse>;
/**
* Returns a single Socket or else a NOT_FOUND code.
*/
GetSocket: grpc.handleUnaryCall<_grpc_channelz_v1_GetSocketRequest__Output, _grpc_channelz_v1_GetSocketResponse>;
/**
* Returns a single Subchannel, or else a NOT_FOUND code.
*/
GetSubchannel: grpc.handleUnaryCall<_grpc_channelz_v1_GetSubchannelRequest__Output, _grpc_channelz_v1_GetSubchannelResponse>;
/**
* Gets all root channels (i.e. channels the application has directly
* created). This does not include subchannels nor non-top level channels.
*/
GetTopChannels: grpc.handleUnaryCall<_grpc_channelz_v1_GetTopChannelsRequest__Output, _grpc_channelz_v1_GetTopChannelsResponse>;
}
export interface ChannelzDefinition extends grpc.ServiceDefinition {
GetChannel: MethodDefinition<_grpc_channelz_v1_GetChannelRequest, _grpc_channelz_v1_GetChannelResponse, _grpc_channelz_v1_GetChannelRequest__Output, _grpc_channelz_v1_GetChannelResponse__Output>
GetServer: MethodDefinition<_grpc_channelz_v1_GetServerRequest, _grpc_channelz_v1_GetServerResponse, _grpc_channelz_v1_GetServerRequest__Output, _grpc_channelz_v1_GetServerResponse__Output>
GetServerSockets: MethodDefinition<_grpc_channelz_v1_GetServerSocketsRequest, _grpc_channelz_v1_GetServerSocketsResponse, _grpc_channelz_v1_GetServerSocketsRequest__Output, _grpc_channelz_v1_GetServerSocketsResponse__Output>
GetServers: MethodDefinition<_grpc_channelz_v1_GetServersRequest, _grpc_channelz_v1_GetServersResponse, _grpc_channelz_v1_GetServersRequest__Output, _grpc_channelz_v1_GetServersResponse__Output>
GetSocket: MethodDefinition<_grpc_channelz_v1_GetSocketRequest, _grpc_channelz_v1_GetSocketResponse, _grpc_channelz_v1_GetSocketRequest__Output, _grpc_channelz_v1_GetSocketResponse__Output>
GetSubchannel: MethodDefinition<_grpc_channelz_v1_GetSubchannelRequest, _grpc_channelz_v1_GetSubchannelResponse, _grpc_channelz_v1_GetSubchannelRequest__Output, _grpc_channelz_v1_GetSubchannelResponse__Output>
GetTopChannels: MethodDefinition<_grpc_channelz_v1_GetTopChannelsRequest, _grpc_channelz_v1_GetTopChannelsResponse, _grpc_channelz_v1_GetTopChannelsRequest__Output, _grpc_channelz_v1_GetTopChannelsResponse__Output>
}
@@ -0,0 +1,17 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetChannelRequest {
/**
* channel_id is the identifier of the specific channel to get.
*/
'channel_id'?: (number | string | Long);
}
export interface GetChannelRequest__Output {
/**
* channel_id is the identifier of the specific channel to get.
*/
'channel_id': (string);
}
@@ -0,0 +1,19 @@
// Original file: proto/channelz.proto
import type { Channel as _grpc_channelz_v1_Channel, Channel__Output as _grpc_channelz_v1_Channel__Output } from '../../../grpc/channelz/v1/Channel';
export interface GetChannelResponse {
/**
* The Channel that corresponds to the requested channel_id. This field
* should be set.
*/
'channel'?: (_grpc_channelz_v1_Channel | null);
}
export interface GetChannelResponse__Output {
/**
* The Channel that corresponds to the requested channel_id. This field
* should be set.
*/
'channel': (_grpc_channelz_v1_Channel__Output | null);
}
@@ -0,0 +1,17 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetServerRequest {
/**
* server_id is the identifier of the specific server to get.
*/
'server_id'?: (number | string | Long);
}
export interface GetServerRequest__Output {
/**
* server_id is the identifier of the specific server to get.
*/
'server_id': (string);
}
@@ -0,0 +1,19 @@
// Original file: proto/channelz.proto
import type { Server as _grpc_channelz_v1_Server, Server__Output as _grpc_channelz_v1_Server__Output } from '../../../grpc/channelz/v1/Server';
export interface GetServerResponse {
/**
* The Server that corresponds to the requested server_id. This field
* should be set.
*/
'server'?: (_grpc_channelz_v1_Server | null);
}
export interface GetServerResponse__Output {
/**
* The Server that corresponds to the requested server_id. This field
* should be set.
*/
'server': (_grpc_channelz_v1_Server__Output | null);
}
@@ -0,0 +1,39 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetServerSocketsRequest {
'server_id'?: (number | string | Long);
/**
* start_socket_id indicates that only sockets at or above this id should be
* included in the results.
* To request the first page, this must be set to 0. To request
* subsequent pages, the client generates this value by adding 1 to
* the highest seen result ID.
*/
'start_socket_id'?: (number | string | Long);
/**
* If non-zero, the server will return a page of results containing
* at most this many items. If zero, the server will choose a
* reasonable page size. Must never be negative.
*/
'max_results'?: (number | string | Long);
}
export interface GetServerSocketsRequest__Output {
'server_id': (string);
/**
* start_socket_id indicates that only sockets at or above this id should be
* included in the results.
* To request the first page, this must be set to 0. To request
* subsequent pages, the client generates this value by adding 1 to
* the highest seen result ID.
*/
'start_socket_id': (string);
/**
* If non-zero, the server will return a page of results containing
* at most this many items. If zero, the server will choose a
* reasonable page size. Must never be negative.
*/
'max_results': (string);
}
@@ -0,0 +1,33 @@
// Original file: proto/channelz.proto
import type { SocketRef as _grpc_channelz_v1_SocketRef, SocketRef__Output as _grpc_channelz_v1_SocketRef__Output } from '../../../grpc/channelz/v1/SocketRef';
export interface GetServerSocketsResponse {
/**
* list of socket refs that the connection detail service knows about. Sorted in
* ascending socket_id order.
* Must contain at least 1 result, otherwise 'end' must be true.
*/
'socket_ref'?: (_grpc_channelz_v1_SocketRef)[];
/**
* If set, indicates that the list of sockets is the final list. Requesting
* more sockets will only return more if they are created after this RPC
* completes.
*/
'end'?: (boolean);
}
export interface GetServerSocketsResponse__Output {
/**
* list of socket refs that the connection detail service knows about. Sorted in
* ascending socket_id order.
* Must contain at least 1 result, otherwise 'end' must be true.
*/
'socket_ref': (_grpc_channelz_v1_SocketRef__Output)[];
/**
* If set, indicates that the list of sockets is the final list. Requesting
* more sockets will only return more if they are created after this RPC
* completes.
*/
'end': (boolean);
}
@@ -0,0 +1,37 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetServersRequest {
/**
* start_server_id indicates that only servers at or above this id should be
* included in the results.
* To request the first page, this must be set to 0. To request
* subsequent pages, the client generates this value by adding 1 to
* the highest seen result ID.
*/
'start_server_id'?: (number | string | Long);
/**
* If non-zero, the server will return a page of results containing
* at most this many items. If zero, the server will choose a
* reasonable page size. Must never be negative.
*/
'max_results'?: (number | string | Long);
}
export interface GetServersRequest__Output {
/**
* start_server_id indicates that only servers at or above this id should be
* included in the results.
* To request the first page, this must be set to 0. To request
* subsequent pages, the client generates this value by adding 1 to
* the highest seen result ID.
*/
'start_server_id': (string);
/**
* If non-zero, the server will return a page of results containing
* at most this many items. If zero, the server will choose a
* reasonable page size. Must never be negative.
*/
'max_results': (string);
}
@@ -0,0 +1,33 @@
// Original file: proto/channelz.proto
import type { Server as _grpc_channelz_v1_Server, Server__Output as _grpc_channelz_v1_Server__Output } from '../../../grpc/channelz/v1/Server';
export interface GetServersResponse {
/**
* list of servers that the connection detail service knows about. Sorted in
* ascending server_id order.
* Must contain at least 1 result, otherwise 'end' must be true.
*/
'server'?: (_grpc_channelz_v1_Server)[];
/**
* If set, indicates that the list of servers is the final list. Requesting
* more servers will only return more if they are created after this RPC
* completes.
*/
'end'?: (boolean);
}
export interface GetServersResponse__Output {
/**
* list of servers that the connection detail service knows about. Sorted in
* ascending server_id order.
* Must contain at least 1 result, otherwise 'end' must be true.
*/
'server': (_grpc_channelz_v1_Server__Output)[];
/**
* If set, indicates that the list of servers is the final list. Requesting
* more servers will only return more if they are created after this RPC
* completes.
*/
'end': (boolean);
}
@@ -0,0 +1,29 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetSocketRequest {
/**
* socket_id is the identifier of the specific socket to get.
*/
'socket_id'?: (number | string | Long);
/**
* If true, the response will contain only high level information
* that is inexpensive to obtain. Fields thay may be omitted are
* documented.
*/
'summary'?: (boolean);
}
export interface GetSocketRequest__Output {
/**
* socket_id is the identifier of the specific socket to get.
*/
'socket_id': (string);
/**
* If true, the response will contain only high level information
* that is inexpensive to obtain. Fields thay may be omitted are
* documented.
*/
'summary': (boolean);
}
@@ -0,0 +1,19 @@
// Original file: proto/channelz.proto
import type { Socket as _grpc_channelz_v1_Socket, Socket__Output as _grpc_channelz_v1_Socket__Output } from '../../../grpc/channelz/v1/Socket';
export interface GetSocketResponse {
/**
* The Socket that corresponds to the requested socket_id. This field
* should be set.
*/
'socket'?: (_grpc_channelz_v1_Socket | null);
}
export interface GetSocketResponse__Output {
/**
* The Socket that corresponds to the requested socket_id. This field
* should be set.
*/
'socket': (_grpc_channelz_v1_Socket__Output | null);
}
@@ -0,0 +1,17 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetSubchannelRequest {
/**
* subchannel_id is the identifier of the specific subchannel to get.
*/
'subchannel_id'?: (number | string | Long);
}
export interface GetSubchannelRequest__Output {
/**
* subchannel_id is the identifier of the specific subchannel to get.
*/
'subchannel_id': (string);
}
@@ -0,0 +1,19 @@
// Original file: proto/channelz.proto
import type { Subchannel as _grpc_channelz_v1_Subchannel, Subchannel__Output as _grpc_channelz_v1_Subchannel__Output } from '../../../grpc/channelz/v1/Subchannel';
export interface GetSubchannelResponse {
/**
* The Subchannel that corresponds to the requested subchannel_id. This
* field should be set.
*/
'subchannel'?: (_grpc_channelz_v1_Subchannel | null);
}
export interface GetSubchannelResponse__Output {
/**
* The Subchannel that corresponds to the requested subchannel_id. This
* field should be set.
*/
'subchannel': (_grpc_channelz_v1_Subchannel__Output | null);
}
@@ -0,0 +1,37 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
export interface GetTopChannelsRequest {
/**
* start_channel_id indicates that only channels at or above this id should be
* included in the results.
* To request the first page, this should be set to 0. To request
* subsequent pages, the client generates this value by adding 1 to
* the highest seen result ID.
*/
'start_channel_id'?: (number | string | Long);
/**
* If non-zero, the server will return a page of results containing
* at most this many items. If zero, the server will choose a
* reasonable page size. Must never be negative.
*/
'max_results'?: (number | string | Long);
}
export interface GetTopChannelsRequest__Output {
/**
* start_channel_id indicates that only channels at or above this id should be
* included in the results.
* To request the first page, this should be set to 0. To request
* subsequent pages, the client generates this value by adding 1 to
* the highest seen result ID.
*/
'start_channel_id': (string);
/**
* If non-zero, the server will return a page of results containing
* at most this many items. If zero, the server will choose a
* reasonable page size. Must never be negative.
*/
'max_results': (string);
}
@@ -0,0 +1,33 @@
// Original file: proto/channelz.proto
import type { Channel as _grpc_channelz_v1_Channel, Channel__Output as _grpc_channelz_v1_Channel__Output } from '../../../grpc/channelz/v1/Channel';
export interface GetTopChannelsResponse {
/**
* list of channels that the connection detail service knows about. Sorted in
* ascending channel_id order.
* Must contain at least 1 result, otherwise 'end' must be true.
*/
'channel'?: (_grpc_channelz_v1_Channel)[];
/**
* If set, indicates that the list of channels is the final list. Requesting
* more channels can only return more if they are created after this RPC
* completes.
*/
'end'?: (boolean);
}
export interface GetTopChannelsResponse__Output {
/**
* list of channels that the connection detail service knows about. Sorted in
* ascending channel_id order.
* Must contain at least 1 result, otherwise 'end' must be true.
*/
'channel': (_grpc_channelz_v1_Channel__Output)[];
/**
* If set, indicates that the list of channels is the final list. Requesting
* more channels can only return more if they are created after this RPC
* completes.
*/
'end': (boolean);
}
+87
View File
@@ -0,0 +1,87 @@
// Original file: proto/channelz.proto
import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any';
export interface _grpc_channelz_v1_Security_OtherSecurity {
/**
* The human readable version of the value.
*/
'name'?: (string);
/**
* The actual security details message.
*/
'value'?: (_google_protobuf_Any | null);
}
export interface _grpc_channelz_v1_Security_OtherSecurity__Output {
/**
* The human readable version of the value.
*/
'name': (string);
/**
* The actual security details message.
*/
'value': (_google_protobuf_Any__Output | null);
}
export interface _grpc_channelz_v1_Security_Tls {
/**
* The cipher suite name in the RFC 4346 format:
* https://tools.ietf.org/html/rfc4346#appendix-C
*/
'standard_name'?: (string);
/**
* Some other way to describe the cipher suite if
* the RFC 4346 name is not available.
*/
'other_name'?: (string);
/**
* the certificate used by this endpoint.
*/
'local_certificate'?: (Buffer | Uint8Array | string);
/**
* the certificate used by the remote endpoint.
*/
'remote_certificate'?: (Buffer | Uint8Array | string);
'cipher_suite'?: "standard_name"|"other_name";
}
export interface _grpc_channelz_v1_Security_Tls__Output {
/**
* The cipher suite name in the RFC 4346 format:
* https://tools.ietf.org/html/rfc4346#appendix-C
*/
'standard_name'?: (string);
/**
* Some other way to describe the cipher suite if
* the RFC 4346 name is not available.
*/
'other_name'?: (string);
/**
* the certificate used by this endpoint.
*/
'local_certificate': (Buffer);
/**
* the certificate used by the remote endpoint.
*/
'remote_certificate': (Buffer);
'cipher_suite': "standard_name"|"other_name";
}
/**
* Security represents details about how secure the socket is.
*/
export interface Security {
'tls'?: (_grpc_channelz_v1_Security_Tls | null);
'other'?: (_grpc_channelz_v1_Security_OtherSecurity | null);
'model'?: "tls"|"other";
}
/**
* Security represents details about how secure the socket is.
*/
export interface Security__Output {
'tls'?: (_grpc_channelz_v1_Security_Tls__Output | null);
'other'?: (_grpc_channelz_v1_Security_OtherSecurity__Output | null);
'model': "tls"|"other";
}
+45
View File
@@ -0,0 +1,45 @@
// Original file: proto/channelz.proto
import type { ServerRef as _grpc_channelz_v1_ServerRef, ServerRef__Output as _grpc_channelz_v1_ServerRef__Output } from '../../../grpc/channelz/v1/ServerRef';
import type { ServerData as _grpc_channelz_v1_ServerData, ServerData__Output as _grpc_channelz_v1_ServerData__Output } from '../../../grpc/channelz/v1/ServerData';
import type { SocketRef as _grpc_channelz_v1_SocketRef, SocketRef__Output as _grpc_channelz_v1_SocketRef__Output } from '../../../grpc/channelz/v1/SocketRef';
/**
* Server represents a single server. There may be multiple servers in a single
* program.
*/
export interface Server {
/**
* The identifier for a Server. This should be set.
*/
'ref'?: (_grpc_channelz_v1_ServerRef | null);
/**
* The associated data of the Server.
*/
'data'?: (_grpc_channelz_v1_ServerData | null);
/**
* The sockets that the server is listening on. There are no ordering
* guarantees. This may be absent.
*/
'listen_socket'?: (_grpc_channelz_v1_SocketRef)[];
}
/**
* Server represents a single server. There may be multiple servers in a single
* program.
*/
export interface Server__Output {
/**
* The identifier for a Server. This should be set.
*/
'ref': (_grpc_channelz_v1_ServerRef__Output | null);
/**
* The associated data of the Server.
*/
'data': (_grpc_channelz_v1_ServerData__Output | null);
/**
* The sockets that the server is listening on. There are no ordering
* guarantees. This may be absent.
*/
'listen_socket': (_grpc_channelz_v1_SocketRef__Output)[];
}
@@ -0,0 +1,57 @@
// Original file: proto/channelz.proto
import type { ChannelTrace as _grpc_channelz_v1_ChannelTrace, ChannelTrace__Output as _grpc_channelz_v1_ChannelTrace__Output } from '../../../grpc/channelz/v1/ChannelTrace';
import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp';
import type { Long } from '@grpc/proto-loader';
/**
* ServerData is data for a specific Server.
*/
export interface ServerData {
/**
* A trace of recent events on the server. May be absent.
*/
'trace'?: (_grpc_channelz_v1_ChannelTrace | null);
/**
* The number of incoming calls started on the server
*/
'calls_started'?: (number | string | Long);
/**
* The number of incoming calls that have completed with an OK status
*/
'calls_succeeded'?: (number | string | Long);
/**
* The number of incoming calls that have a completed with a non-OK status
*/
'calls_failed'?: (number | string | Long);
/**
* The last time a call was started on the server.
*/
'last_call_started_timestamp'?: (_google_protobuf_Timestamp | null);
}
/**
* ServerData is data for a specific Server.
*/
export interface ServerData__Output {
/**
* A trace of recent events on the server. May be absent.
*/
'trace': (_grpc_channelz_v1_ChannelTrace__Output | null);
/**
* The number of incoming calls started on the server
*/
'calls_started': (string);
/**
* The number of incoming calls that have completed with an OK status
*/
'calls_succeeded': (string);
/**
* The number of incoming calls that have a completed with a non-OK status
*/
'calls_failed': (string);
/**
* The last time a call was started on the server.
*/
'last_call_started_timestamp': (_google_protobuf_Timestamp__Output | null);
}
+31
View File
@@ -0,0 +1,31 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
/**
* ServerRef is a reference to a Server.
*/
export interface ServerRef {
/**
* A globally unique identifier for this server. Must be a positive number.
*/
'server_id'?: (number | string | Long);
/**
* An optional name associated with the server.
*/
'name'?: (string);
}
/**
* ServerRef is a reference to a Server.
*/
export interface ServerRef__Output {
/**
* A globally unique identifier for this server. Must be a positive number.
*/
'server_id': (string);
/**
* An optional name associated with the server.
*/
'name': (string);
}
+70
View File
@@ -0,0 +1,70 @@
// Original file: proto/channelz.proto
import type { SocketRef as _grpc_channelz_v1_SocketRef, SocketRef__Output as _grpc_channelz_v1_SocketRef__Output } from '../../../grpc/channelz/v1/SocketRef';
import type { SocketData as _grpc_channelz_v1_SocketData, SocketData__Output as _grpc_channelz_v1_SocketData__Output } from '../../../grpc/channelz/v1/SocketData';
import type { Address as _grpc_channelz_v1_Address, Address__Output as _grpc_channelz_v1_Address__Output } from '../../../grpc/channelz/v1/Address';
import type { Security as _grpc_channelz_v1_Security, Security__Output as _grpc_channelz_v1_Security__Output } from '../../../grpc/channelz/v1/Security';
/**
* Information about an actual connection. Pronounced "sock-ay".
*/
export interface Socket {
/**
* The identifier for the Socket.
*/
'ref'?: (_grpc_channelz_v1_SocketRef | null);
/**
* Data specific to this Socket.
*/
'data'?: (_grpc_channelz_v1_SocketData | null);
/**
* The locally bound address.
*/
'local'?: (_grpc_channelz_v1_Address | null);
/**
* The remote bound address. May be absent.
*/
'remote'?: (_grpc_channelz_v1_Address | null);
/**
* Security details for this socket. May be absent if not available, or
* there is no security on the socket.
*/
'security'?: (_grpc_channelz_v1_Security | null);
/**
* Optional, represents the name of the remote endpoint, if different than
* the original target name.
*/
'remote_name'?: (string);
}
/**
* Information about an actual connection. Pronounced "sock-ay".
*/
export interface Socket__Output {
/**
* The identifier for the Socket.
*/
'ref': (_grpc_channelz_v1_SocketRef__Output | null);
/**
* Data specific to this Socket.
*/
'data': (_grpc_channelz_v1_SocketData__Output | null);
/**
* The locally bound address.
*/
'local': (_grpc_channelz_v1_Address__Output | null);
/**
* The remote bound address. May be absent.
*/
'remote': (_grpc_channelz_v1_Address__Output | null);
/**
* Security details for this socket. May be absent if not available, or
* there is no security on the socket.
*/
'security': (_grpc_channelz_v1_Security__Output | null);
/**
* Optional, represents the name of the remote endpoint, if different than
* the original target name.
*/
'remote_name': (string);
}
+150
View File
@@ -0,0 +1,150 @@
// Original file: proto/channelz.proto
import type { Timestamp as _google_protobuf_Timestamp, Timestamp__Output as _google_protobuf_Timestamp__Output } from '../../../google/protobuf/Timestamp';
import type { Int64Value as _google_protobuf_Int64Value, Int64Value__Output as _google_protobuf_Int64Value__Output } from '../../../google/protobuf/Int64Value';
import type { SocketOption as _grpc_channelz_v1_SocketOption, SocketOption__Output as _grpc_channelz_v1_SocketOption__Output } from '../../../grpc/channelz/v1/SocketOption';
import type { Long } from '@grpc/proto-loader';
/**
* SocketData is data associated for a specific Socket. The fields present
* are specific to the implementation, so there may be minor differences in
* the semantics. (e.g. flow control windows)
*/
export interface SocketData {
/**
* The number of streams that have been started.
*/
'streams_started'?: (number | string | Long);
/**
* The number of streams that have ended successfully:
* On client side, received frame with eos bit set;
* On server side, sent frame with eos bit set.
*/
'streams_succeeded'?: (number | string | Long);
/**
* The number of streams that have ended unsuccessfully:
* On client side, ended without receiving frame with eos bit set;
* On server side, ended without sending frame with eos bit set.
*/
'streams_failed'?: (number | string | Long);
/**
* The number of grpc messages successfully sent on this socket.
*/
'messages_sent'?: (number | string | Long);
/**
* The number of grpc messages received on this socket.
*/
'messages_received'?: (number | string | Long);
/**
* The number of keep alives sent. This is typically implemented with HTTP/2
* ping messages.
*/
'keep_alives_sent'?: (number | string | Long);
/**
* The last time a stream was created by this endpoint. Usually unset for
* servers.
*/
'last_local_stream_created_timestamp'?: (_google_protobuf_Timestamp | null);
/**
* The last time a stream was created by the remote endpoint. Usually unset
* for clients.
*/
'last_remote_stream_created_timestamp'?: (_google_protobuf_Timestamp | null);
/**
* The last time a message was sent by this endpoint.
*/
'last_message_sent_timestamp'?: (_google_protobuf_Timestamp | null);
/**
* The last time a message was received by this endpoint.
*/
'last_message_received_timestamp'?: (_google_protobuf_Timestamp | null);
/**
* The amount of window, granted to the local endpoint by the remote endpoint.
* This may be slightly out of date due to network latency. This does NOT
* include stream level or TCP level flow control info.
*/
'local_flow_control_window'?: (_google_protobuf_Int64Value | null);
/**
* The amount of window, granted to the remote endpoint by the local endpoint.
* This may be slightly out of date due to network latency. This does NOT
* include stream level or TCP level flow control info.
*/
'remote_flow_control_window'?: (_google_protobuf_Int64Value | null);
/**
* Socket options set on this socket. May be absent if 'summary' is set
* on GetSocketRequest.
*/
'option'?: (_grpc_channelz_v1_SocketOption)[];
}
/**
* SocketData is data associated for a specific Socket. The fields present
* are specific to the implementation, so there may be minor differences in
* the semantics. (e.g. flow control windows)
*/
export interface SocketData__Output {
/**
* The number of streams that have been started.
*/
'streams_started': (string);
/**
* The number of streams that have ended successfully:
* On client side, received frame with eos bit set;
* On server side, sent frame with eos bit set.
*/
'streams_succeeded': (string);
/**
* The number of streams that have ended unsuccessfully:
* On client side, ended without receiving frame with eos bit set;
* On server side, ended without sending frame with eos bit set.
*/
'streams_failed': (string);
/**
* The number of grpc messages successfully sent on this socket.
*/
'messages_sent': (string);
/**
* The number of grpc messages received on this socket.
*/
'messages_received': (string);
/**
* The number of keep alives sent. This is typically implemented with HTTP/2
* ping messages.
*/
'keep_alives_sent': (string);
/**
* The last time a stream was created by this endpoint. Usually unset for
* servers.
*/
'last_local_stream_created_timestamp': (_google_protobuf_Timestamp__Output | null);
/**
* The last time a stream was created by the remote endpoint. Usually unset
* for clients.
*/
'last_remote_stream_created_timestamp': (_google_protobuf_Timestamp__Output | null);
/**
* The last time a message was sent by this endpoint.
*/
'last_message_sent_timestamp': (_google_protobuf_Timestamp__Output | null);
/**
* The last time a message was received by this endpoint.
*/
'last_message_received_timestamp': (_google_protobuf_Timestamp__Output | null);
/**
* The amount of window, granted to the local endpoint by the remote endpoint.
* This may be slightly out of date due to network latency. This does NOT
* include stream level or TCP level flow control info.
*/
'local_flow_control_window': (_google_protobuf_Int64Value__Output | null);
/**
* The amount of window, granted to the remote endpoint by the local endpoint.
* This may be slightly out of date due to network latency. This does NOT
* include stream level or TCP level flow control info.
*/
'remote_flow_control_window': (_google_protobuf_Int64Value__Output | null);
/**
* Socket options set on this socket. May be absent if 'summary' is set
* on GetSocketRequest.
*/
'option': (_grpc_channelz_v1_SocketOption__Output)[];
}
@@ -0,0 +1,47 @@
// Original file: proto/channelz.proto
import type { Any as _google_protobuf_Any, Any__Output as _google_protobuf_Any__Output } from '../../../google/protobuf/Any';
/**
* SocketOption represents socket options for a socket. Specifically, these
* are the options returned by getsockopt().
*/
export interface SocketOption {
/**
* The full name of the socket option. Typically this will be the upper case
* name, such as "SO_REUSEPORT".
*/
'name'?: (string);
/**
* The human readable value of this socket option. At least one of value or
* additional will be set.
*/
'value'?: (string);
/**
* Additional data associated with the socket option. At least one of value
* or additional will be set.
*/
'additional'?: (_google_protobuf_Any | null);
}
/**
* SocketOption represents socket options for a socket. Specifically, these
* are the options returned by getsockopt().
*/
export interface SocketOption__Output {
/**
* The full name of the socket option. Typically this will be the upper case
* name, such as "SO_REUSEPORT".
*/
'name': (string);
/**
* The human readable value of this socket option. At least one of value or
* additional will be set.
*/
'value': (string);
/**
* Additional data associated with the socket option. At least one of value
* or additional will be set.
*/
'additional': (_google_protobuf_Any__Output | null);
}
@@ -0,0 +1,33 @@
// Original file: proto/channelz.proto
import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../google/protobuf/Duration';
/**
* For use with SocketOption's additional field. This is primarily used for
* SO_LINGER.
*/
export interface SocketOptionLinger {
/**
* active maps to `struct linger.l_onoff`
*/
'active'?: (boolean);
/**
* duration maps to `struct linger.l_linger`
*/
'duration'?: (_google_protobuf_Duration | null);
}
/**
* For use with SocketOption's additional field. This is primarily used for
* SO_LINGER.
*/
export interface SocketOptionLinger__Output {
/**
* active maps to `struct linger.l_onoff`
*/
'active': (boolean);
/**
* duration maps to `struct linger.l_linger`
*/
'duration': (_google_protobuf_Duration__Output | null);
}
@@ -0,0 +1,74 @@
// Original file: proto/channelz.proto
/**
* For use with SocketOption's additional field. Tcp info for
* SOL_TCP and TCP_INFO.
*/
export interface SocketOptionTcpInfo {
'tcpi_state'?: (number);
'tcpi_ca_state'?: (number);
'tcpi_retransmits'?: (number);
'tcpi_probes'?: (number);
'tcpi_backoff'?: (number);
'tcpi_options'?: (number);
'tcpi_snd_wscale'?: (number);
'tcpi_rcv_wscale'?: (number);
'tcpi_rto'?: (number);
'tcpi_ato'?: (number);
'tcpi_snd_mss'?: (number);
'tcpi_rcv_mss'?: (number);
'tcpi_unacked'?: (number);
'tcpi_sacked'?: (number);
'tcpi_lost'?: (number);
'tcpi_retrans'?: (number);
'tcpi_fackets'?: (number);
'tcpi_last_data_sent'?: (number);
'tcpi_last_ack_sent'?: (number);
'tcpi_last_data_recv'?: (number);
'tcpi_last_ack_recv'?: (number);
'tcpi_pmtu'?: (number);
'tcpi_rcv_ssthresh'?: (number);
'tcpi_rtt'?: (number);
'tcpi_rttvar'?: (number);
'tcpi_snd_ssthresh'?: (number);
'tcpi_snd_cwnd'?: (number);
'tcpi_advmss'?: (number);
'tcpi_reordering'?: (number);
}
/**
* For use with SocketOption's additional field. Tcp info for
* SOL_TCP and TCP_INFO.
*/
export interface SocketOptionTcpInfo__Output {
'tcpi_state': (number);
'tcpi_ca_state': (number);
'tcpi_retransmits': (number);
'tcpi_probes': (number);
'tcpi_backoff': (number);
'tcpi_options': (number);
'tcpi_snd_wscale': (number);
'tcpi_rcv_wscale': (number);
'tcpi_rto': (number);
'tcpi_ato': (number);
'tcpi_snd_mss': (number);
'tcpi_rcv_mss': (number);
'tcpi_unacked': (number);
'tcpi_sacked': (number);
'tcpi_lost': (number);
'tcpi_retrans': (number);
'tcpi_fackets': (number);
'tcpi_last_data_sent': (number);
'tcpi_last_ack_sent': (number);
'tcpi_last_data_recv': (number);
'tcpi_last_ack_recv': (number);
'tcpi_pmtu': (number);
'tcpi_rcv_ssthresh': (number);
'tcpi_rtt': (number);
'tcpi_rttvar': (number);
'tcpi_snd_ssthresh': (number);
'tcpi_snd_cwnd': (number);
'tcpi_advmss': (number);
'tcpi_reordering': (number);
}
@@ -0,0 +1,19 @@
// Original file: proto/channelz.proto
import type { Duration as _google_protobuf_Duration, Duration__Output as _google_protobuf_Duration__Output } from '../../../google/protobuf/Duration';
/**
* For use with SocketOption's additional field. This is primarily used for
* SO_RCVTIMEO and SO_SNDTIMEO
*/
export interface SocketOptionTimeout {
'duration'?: (_google_protobuf_Duration | null);
}
/**
* For use with SocketOption's additional field. This is primarily used for
* SO_RCVTIMEO and SO_SNDTIMEO
*/
export interface SocketOptionTimeout__Output {
'duration': (_google_protobuf_Duration__Output | null);
}
+31
View File
@@ -0,0 +1,31 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
/**
* SocketRef is a reference to a Socket.
*/
export interface SocketRef {
/**
* The globally unique id for this socket. Must be a positive number.
*/
'socket_id'?: (number | string | Long);
/**
* An optional name associated with the socket.
*/
'name'?: (string);
}
/**
* SocketRef is a reference to a Socket.
*/
export interface SocketRef__Output {
/**
* The globally unique id for this socket. Must be a positive number.
*/
'socket_id': (string);
/**
* An optional name associated with the socket.
*/
'name': (string);
}
@@ -0,0 +1,70 @@
// Original file: proto/channelz.proto
import type { SubchannelRef as _grpc_channelz_v1_SubchannelRef, SubchannelRef__Output as _grpc_channelz_v1_SubchannelRef__Output } from '../../../grpc/channelz/v1/SubchannelRef';
import type { ChannelData as _grpc_channelz_v1_ChannelData, ChannelData__Output as _grpc_channelz_v1_ChannelData__Output } from '../../../grpc/channelz/v1/ChannelData';
import type { ChannelRef as _grpc_channelz_v1_ChannelRef, ChannelRef__Output as _grpc_channelz_v1_ChannelRef__Output } from '../../../grpc/channelz/v1/ChannelRef';
import type { SocketRef as _grpc_channelz_v1_SocketRef, SocketRef__Output as _grpc_channelz_v1_SocketRef__Output } from '../../../grpc/channelz/v1/SocketRef';
/**
* Subchannel is a logical grouping of channels, subchannels, and sockets.
* A subchannel is load balanced over by it's ancestor
*/
export interface Subchannel {
/**
* The identifier for this channel.
*/
'ref'?: (_grpc_channelz_v1_SubchannelRef | null);
/**
* Data specific to this channel.
*/
'data'?: (_grpc_channelz_v1_ChannelData | null);
/**
* There are no ordering guarantees on the order of channel refs.
* There may not be cycles in the ref graph.
* A channel ref may be present in more than one channel or subchannel.
*/
'channel_ref'?: (_grpc_channelz_v1_ChannelRef)[];
/**
* At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
* There are no ordering guarantees on the order of subchannel refs.
* There may not be cycles in the ref graph.
* A sub channel ref may be present in more than one channel or subchannel.
*/
'subchannel_ref'?: (_grpc_channelz_v1_SubchannelRef)[];
/**
* There are no ordering guarantees on the order of sockets.
*/
'socket_ref'?: (_grpc_channelz_v1_SocketRef)[];
}
/**
* Subchannel is a logical grouping of channels, subchannels, and sockets.
* A subchannel is load balanced over by it's ancestor
*/
export interface Subchannel__Output {
/**
* The identifier for this channel.
*/
'ref': (_grpc_channelz_v1_SubchannelRef__Output | null);
/**
* Data specific to this channel.
*/
'data': (_grpc_channelz_v1_ChannelData__Output | null);
/**
* There are no ordering guarantees on the order of channel refs.
* There may not be cycles in the ref graph.
* A channel ref may be present in more than one channel or subchannel.
*/
'channel_ref': (_grpc_channelz_v1_ChannelRef__Output)[];
/**
* At most one of 'channel_ref+subchannel_ref' and 'socket' is set.
* There are no ordering guarantees on the order of subchannel refs.
* There may not be cycles in the ref graph.
* A sub channel ref may be present in more than one channel or subchannel.
*/
'subchannel_ref': (_grpc_channelz_v1_SubchannelRef__Output)[];
/**
* There are no ordering guarantees on the order of sockets.
*/
'socket_ref': (_grpc_channelz_v1_SocketRef__Output)[];
}
@@ -0,0 +1,31 @@
// Original file: proto/channelz.proto
import type { Long } from '@grpc/proto-loader';
/**
* SubchannelRef is a reference to a Subchannel.
*/
export interface SubchannelRef {
/**
* The globally unique id for this subchannel. Must be a positive number.
*/
'subchannel_id'?: (number | string | Long);
/**
* An optional name associated with the subchannel.
*/
'name'?: (string);
}
/**
* SubchannelRef is a reference to a Subchannel.
*/
export interface SubchannelRef__Output {
/**
* The globally unique id for this subchannel. Must be a positive number.
*/
'subchannel_id': (string);
/**
* An optional name associated with the subchannel.
*/
'name': (string);
}
+298
View File
@@ -0,0 +1,298 @@
/*
* 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 { log } from './logging';
import { LogVerbosity } from './constants';
import { getDefaultAuthority } from './resolver';
import { Socket } from 'net';
import * as http from 'http';
import * as tls from 'tls';
import * as logging from './logging';
import {
SubchannelAddress,
isTcpSubchannelAddress,
subchannelAddressToString,
} from './subchannel-address';
import { ChannelOptions } from './channel-options';
import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
import { URL } from 'url';
const TRACER_NAME = 'proxy';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
interface ProxyInfo {
address?: string;
creds?: string;
}
function getProxyInfo(): ProxyInfo {
let proxyEnv = '';
let envVar = '';
/* Prefer using 'grpc_proxy'. Fallback on 'http_proxy' if it is not set.
* Also prefer using 'https_proxy' with fallback on 'http_proxy'. The
* fallback behavior can be removed if there's a demand for it.
*/
if (process.env.grpc_proxy) {
envVar = 'grpc_proxy';
proxyEnv = process.env.grpc_proxy;
} else if (process.env.https_proxy) {
envVar = 'https_proxy';
proxyEnv = process.env.https_proxy;
} else if (process.env.http_proxy) {
envVar = 'http_proxy';
proxyEnv = process.env.http_proxy;
} else {
return {};
}
let proxyUrl: URL;
try {
proxyUrl = new URL(proxyEnv);
} catch (e) {
log(LogVerbosity.ERROR, `cannot parse value of "${envVar}" env var`);
return {};
}
if (proxyUrl.protocol !== 'http:') {
log(
LogVerbosity.ERROR,
`"${proxyUrl.protocol}" scheme not supported in proxy URI`
);
return {};
}
let userCred: string | null = null;
if (proxyUrl.username) {
if (proxyUrl.password) {
log(LogVerbosity.INFO, 'userinfo found in proxy URI');
userCred = `${proxyUrl.username}:${proxyUrl.password}`;
} else {
userCred = proxyUrl.username;
}
}
const hostname = proxyUrl.hostname;
let port = proxyUrl.port;
/* The proxy URL uses the scheme "http:", which has a default port number of
* 80. We need to set that explicitly here if it is omitted because otherwise
* it will use gRPC's default port 443. */
if (port === '') {
port = '80';
}
const result: ProxyInfo = {
address: `${hostname}:${port}`,
};
if (userCred) {
result.creds = userCred;
}
trace(
'Proxy server ' + result.address + ' set by environment variable ' + envVar
);
return result;
}
function getNoProxyHostList(): string[] {
/* Prefer using 'no_grpc_proxy'. Fallback on 'no_proxy' if it is not set. */
let noProxyStr: string | undefined = process.env.no_grpc_proxy;
let envVar = 'no_grpc_proxy';
if (!noProxyStr) {
noProxyStr = process.env.no_proxy;
envVar = 'no_proxy';
}
if (noProxyStr) {
trace('No proxy server list set by environment variable ' + envVar);
return noProxyStr.split(',');
} else {
return [];
}
}
export interface ProxyMapResult {
target: GrpcUri;
extraOptions: ChannelOptions;
}
export function mapProxyName(
target: GrpcUri,
options: ChannelOptions
): ProxyMapResult {
const noProxyResult: ProxyMapResult = {
target: target,
extraOptions: {},
};
if ((options['grpc.enable_http_proxy'] ?? 1) === 0) {
return noProxyResult;
}
const proxyInfo = getProxyInfo();
if (!proxyInfo.address) {
return noProxyResult;
}
const hostPort = splitHostPort(target.path);
if (!hostPort) {
return noProxyResult;
}
const serverHost = hostPort.host;
for (const host of getNoProxyHostList()) {
if (host === serverHost) {
trace(
'Not using proxy for target in no_proxy list: ' + uriToString(target)
);
return noProxyResult;
}
}
const extraOptions: ChannelOptions = {
'grpc.http_connect_target': uriToString(target),
};
if (proxyInfo.creds) {
extraOptions['grpc.http_connect_creds'] = proxyInfo.creds;
}
return {
target: {
scheme: 'dns',
path: proxyInfo.address,
},
extraOptions: extraOptions,
};
}
export interface ProxyConnectionResult {
socket?: Socket;
realTarget?: GrpcUri;
}
export function getProxiedConnection(
address: SubchannelAddress,
channelOptions: ChannelOptions,
connectionOptions: tls.ConnectionOptions
): Promise<ProxyConnectionResult> {
if (!('grpc.http_connect_target' in channelOptions)) {
return Promise.resolve<ProxyConnectionResult>({});
}
const realTarget = channelOptions['grpc.http_connect_target'] as string;
const parsedTarget = parseUri(realTarget);
if (parsedTarget === null) {
return Promise.resolve<ProxyConnectionResult>({});
}
const options: http.RequestOptions = {
method: 'CONNECT',
path: parsedTarget.path,
};
const headers: http.OutgoingHttpHeaders = {
Host: parsedTarget.path,
};
// Connect to the subchannel address as a proxy
if (isTcpSubchannelAddress(address)) {
options.host = address.host;
options.port = address.port;
} else {
options.socketPath = address.path;
}
if ('grpc.http_connect_creds' in channelOptions) {
headers['Proxy-Authorization'] =
'Basic ' +
Buffer.from(
channelOptions['grpc.http_connect_creds'] as string
).toString('base64');
}
options.headers = headers
const proxyAddressString = subchannelAddressToString(address);
trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path);
return new Promise<ProxyConnectionResult>((resolve, reject) => {
const request = http.request(options);
request.once('connect', (res, socket, head) => {
request.removeAllListeners();
socket.removeAllListeners();
if (res.statusCode === 200) {
trace(
'Successfully connected to ' +
options.path +
' through proxy ' +
proxyAddressString
);
if ('secureContext' in connectionOptions) {
/* The proxy is connecting to a TLS server, so upgrade this socket
* connection to a TLS connection.
* This is a workaround for https://github.com/nodejs/node/issues/32922
* See https://github.com/grpc/grpc-node/pull/1369 for more info. */
const targetPath = getDefaultAuthority(parsedTarget);
const hostPort = splitHostPort(targetPath);
const remoteHost = hostPort?.host ?? targetPath;
const cts = tls.connect(
{
host: remoteHost,
servername: remoteHost,
socket: socket,
...connectionOptions,
},
() => {
trace(
'Successfully established a TLS connection to ' +
options.path +
' through proxy ' +
proxyAddressString
);
resolve({ socket: cts, realTarget: parsedTarget });
}
);
cts.on('error', (error: Error) => {
trace('Failed to establish a TLS connection to ' +
options.path +
' through proxy ' +
proxyAddressString +
' with error ' +
error.message);
reject();
});
} else {
trace(
'Successfully established a plaintext connection to ' +
options.path +
' through proxy ' +
proxyAddressString
);
resolve({
socket,
realTarget: parsedTarget,
});
}
} else {
log(
LogVerbosity.ERROR,
'Failed to connect to ' +
options.path +
' through proxy ' +
proxyAddressString +
' with status ' +
res.statusCode
);
reject();
}
});
request.once('error', (err) => {
request.removeAllListeners();
log(
LogVerbosity.ERROR,
'Failed to connect to proxy ' +
proxyAddressString +
' with error ' +
err.message
);
reject();
});
request.end();
});
}
+279
View File
@@ -0,0 +1,279 @@
/*
* 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 {
ClientDuplexStream,
ClientReadableStream,
ClientUnaryCall,
ClientWritableStream,
ServiceError,
} from './call';
import { CallCredentials, OAuth2Client } from './call-credentials';
import { Deadline, StatusObject } from './call-stream';
import { Channel, ChannelImplementation } from './channel';
import { ConnectivityState } from './connectivity-state';
import { ChannelCredentials } from './channel-credentials';
import {
CallOptions,
Client,
ClientOptions,
CallInvocationTransformer,
CallProperties,
UnaryCallback,
} from './client';
import { LogVerbosity, Status, Propagate } from './constants';
import * as logging from './logging';
import {
Deserialize,
loadPackageDefinition,
makeClientConstructor,
MethodDefinition,
Serialize,
ServiceDefinition,
} from './make-client';
import { Metadata, MetadataValue } from './metadata';
import {
Server,
UntypedHandleCall,
UntypedServiceImplementation,
} from './server';
import { KeyCertPair, ServerCredentials } from './server-credentials';
import { StatusBuilder } from './status-builder';
import {
handleBidiStreamingCall,
handleServerStreamingCall,
handleClientStreamingCall,
handleUnaryCall,
sendUnaryData,
ServerUnaryCall,
ServerReadableStream,
ServerWritableStream,
ServerDuplexStream,
ServerErrorResponse,
} from './server-call';
export { OAuth2Client };
/**** Client Credentials ****/
// Using assign only copies enumerable properties, which is what we want
export const credentials = {
/**
* Combine a ChannelCredentials with any number of CallCredentials into a
* single ChannelCredentials object.
* @param channelCredentials The ChannelCredentials object.
* @param callCredentials Any number of CallCredentials objects.
* @return The resulting ChannelCredentials object.
*/
combineChannelCredentials: (
channelCredentials: ChannelCredentials,
...callCredentials: CallCredentials[]
): ChannelCredentials => {
return callCredentials.reduce(
(acc, other) => acc.compose(other),
channelCredentials
);
},
/**
* Combine any number of CallCredentials into a single CallCredentials
* object.
* @param first The first CallCredentials object.
* @param additional Any number of additional CallCredentials objects.
* @return The resulting CallCredentials object.
*/
combineCallCredentials: (
first: CallCredentials,
...additional: CallCredentials[]
): CallCredentials => {
return additional.reduce((acc, other) => acc.compose(other), first);
},
// from channel-credentials.ts
createInsecure: ChannelCredentials.createInsecure,
createSsl: ChannelCredentials.createSsl,
// from call-credentials.ts
createFromMetadataGenerator: CallCredentials.createFromMetadataGenerator,
createFromGoogleCredential: CallCredentials.createFromGoogleCredential,
createEmpty: CallCredentials.createEmpty,
};
/**** Metadata ****/
export { Metadata, MetadataValue };
/**** Constants ****/
export {
LogVerbosity as logVerbosity,
Status as status,
ConnectivityState as connectivityState,
Propagate as propagate,
// TODO: Other constants as well
};
/**** Client ****/
export {
Client,
ClientOptions,
loadPackageDefinition,
makeClientConstructor,
makeClientConstructor as makeGenericClientConstructor,
CallProperties,
CallInvocationTransformer,
ChannelImplementation as Channel,
Channel as ChannelInterface,
UnaryCallback as requestCallback,
};
/**
* Close a Client object.
* @param client The client to close.
*/
export const closeClient = (client: Client) => client.close();
export const waitForClientReady = (
client: Client,
deadline: Date | number,
callback: (error?: Error) => void
) => client.waitForReady(deadline, callback);
/* Interfaces */
export {
sendUnaryData,
ChannelCredentials,
CallCredentials,
Deadline,
Serialize as serialize,
Deserialize as deserialize,
ClientUnaryCall,
ClientReadableStream,
ClientWritableStream,
ClientDuplexStream,
CallOptions,
MethodDefinition,
StatusObject,
ServiceError,
ServerUnaryCall,
ServerReadableStream,
ServerWritableStream,
ServerDuplexStream,
ServerErrorResponse,
ServiceDefinition,
UntypedHandleCall,
UntypedServiceImplementation,
};
/**** Server ****/
export {
handleBidiStreamingCall,
handleServerStreamingCall,
handleUnaryCall,
handleClientStreamingCall,
};
/* eslint-disable @typescript-eslint/no-explicit-any */
export type Call =
| ClientUnaryCall
| ClientReadableStream<any>
| ClientWritableStream<any>
| ClientDuplexStream<any, any>;
/* eslint-enable @typescript-eslint/no-explicit-any */
/**** Unimplemented function stubs ****/
/* eslint-disable @typescript-eslint/no-explicit-any */
export const loadObject = (value: any, options: any) => {
throw new Error(
'Not available in this library. Use @grpc/proto-loader and loadPackageDefinition instead'
);
};
export const load = (filename: any, format: any, options: any) => {
throw new Error(
'Not available in this library. Use @grpc/proto-loader and loadPackageDefinition instead'
);
};
export const setLogger = (logger: Partial<Console>): void => {
logging.setLogger(logger);
};
export const setLogVerbosity = (verbosity: LogVerbosity): void => {
logging.setLoggerVerbosity(verbosity);
};
export { Server };
export { ServerCredentials };
export { KeyCertPair };
export const getClientChannel = (client: Client) => {
return Client.prototype.getChannel.call(client);
};
export { StatusBuilder };
export { Listener } from './call-stream';
export {
Requester,
ListenerBuilder,
RequesterBuilder,
Interceptor,
InterceptorOptions,
InterceptorProvider,
InterceptingCall,
InterceptorConfigurationError,
} from './client-interceptors';
export { GrpcObject } from './make-client';
export { ChannelOptions } from './channel-options';
export {
getChannelzServiceDefinition,
getChannelzHandlers
} from './channelz';
export { addAdminServicesToServer } from './admin';
import * as experimental from './experimental';
export { experimental };
import * as resolver_dns from './resolver-dns';
import * as resolver_uds from './resolver-uds';
import * as resolver_ip from './resolver-ip';
import * as load_balancer_pick_first from './load-balancer-pick-first';
import * as load_balancer_round_robin from './load-balancer-round-robin';
import * as channelz from './channelz';
const clientVersion = require('../../package.json').version;
(() => {
logging.trace(LogVerbosity.DEBUG, 'index', 'Loading @grpc/grpc-js version ' + clientVersion);
resolver_dns.setup();
resolver_uds.setup();
resolver_ip.setup();
load_balancer_pick_first.setup();
load_balancer_round_robin.setup();
channelz.setup();
})();
+155
View File
@@ -0,0 +1,155 @@
/*
* Copyright 2020 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 {
LoadBalancer,
ChannelControlHelper,
LoadBalancingConfig,
createLoadBalancer,
} from './load-balancer';
import { Subchannel } from './subchannel';
import { SubchannelAddress } from './subchannel-address';
import { ChannelOptions } from './channel-options';
import { ConnectivityState } from './connectivity-state';
import { Picker } from './picker';
import { ChannelRef, SubchannelRef } from './channelz';
const TYPE_NAME = 'child_load_balancer_helper';
export class ChildLoadBalancerHandler implements LoadBalancer {
private currentChild: LoadBalancer | null = null;
private pendingChild: LoadBalancer | null = null;
private ChildPolicyHelper = class {
private child: LoadBalancer | null = null;
constructor(private parent: ChildLoadBalancerHandler) {}
createSubchannel(
subchannelAddress: SubchannelAddress,
subchannelArgs: ChannelOptions
): Subchannel {
return this.parent.channelControlHelper.createSubchannel(
subchannelAddress,
subchannelArgs
);
}
updateState(connectivityState: ConnectivityState, picker: Picker): void {
if (this.calledByPendingChild()) {
if (connectivityState !== ConnectivityState.READY) {
return;
}
this.parent.currentChild?.destroy();
this.parent.currentChild = this.parent.pendingChild;
this.parent.pendingChild = null;
} else if (!this.calledByCurrentChild()) {
return;
}
this.parent.channelControlHelper.updateState(connectivityState, picker);
}
requestReresolution(): void {
const latestChild = this.parent.pendingChild ?? this.parent.currentChild;
if (this.child === latestChild) {
this.parent.channelControlHelper.requestReresolution();
}
}
setChild(newChild: LoadBalancer) {
this.child = newChild;
}
addChannelzChild(child: ChannelRef | SubchannelRef) {
this.parent.channelControlHelper.addChannelzChild(child);
}
removeChannelzChild(child: ChannelRef | SubchannelRef) {
this.parent.channelControlHelper.removeChannelzChild(child);
}
private calledByPendingChild(): boolean {
return this.child === this.parent.pendingChild;
}
private calledByCurrentChild(): boolean {
return this.child === this.parent.currentChild;
}
};
constructor(private readonly channelControlHelper: ChannelControlHelper) {}
/**
* Prerequisites: lbConfig !== null and lbConfig.name is registered
* @param addressList
* @param lbConfig
* @param attributes
*/
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig,
attributes: { [key: string]: unknown }
): void {
let childToUpdate: LoadBalancer;
if (
this.currentChild === null ||
this.currentChild.getTypeName() !== lbConfig.getLoadBalancerName()
) {
const newHelper = new this.ChildPolicyHelper(this);
const newChild = createLoadBalancer(lbConfig, newHelper)!;
newHelper.setChild(newChild);
if (this.currentChild === null) {
this.currentChild = newChild;
childToUpdate = this.currentChild;
} else {
if (this.pendingChild) {
this.pendingChild.destroy();
}
this.pendingChild = newChild;
childToUpdate = this.pendingChild;
}
} else {
if (this.pendingChild === null) {
childToUpdate = this.currentChild;
} else {
childToUpdate = this.pendingChild;
}
}
childToUpdate.updateAddressList(addressList, lbConfig, attributes);
}
exitIdle(): void {
if (this.currentChild) {
this.currentChild.resetBackoff();
if (this.pendingChild) {
this.pendingChild.resetBackoff();
}
}
}
resetBackoff(): void {
if (this.currentChild) {
this.currentChild.resetBackoff();
if (this.pendingChild) {
this.pendingChild.resetBackoff();
}
}
}
destroy(): void {
if (this.currentChild) {
this.currentChild.destroy();
this.currentChild = null;
}
if (this.pendingChild) {
this.pendingChild.destroy();
this.pendingChild = null;
}
}
getTypeName(): string {
return TYPE_NAME;
}
}
+472
View File
@@ -0,0 +1,472 @@
/*
* 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 {
LoadBalancer,
ChannelControlHelper,
LoadBalancingConfig,
registerDefaultLoadBalancerType,
registerLoadBalancerType,
} from './load-balancer';
import { ConnectivityState } from './connectivity-state';
import {
QueuePicker,
Picker,
PickArgs,
CompletePickResult,
PickResultType,
UnavailablePicker,
} from './picker';
import { Subchannel, ConnectivityStateListener } from './subchannel';
import {
SubchannelAddress,
subchannelAddressToString,
} from './subchannel-address';
import * as logging from './logging';
import { LogVerbosity } from './constants';
const TRACER_NAME = 'pick_first';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
const TYPE_NAME = 'pick_first';
/**
* Delay after starting a connection on a subchannel before starting a
* connection on the next subchannel in the list, for Happy Eyeballs algorithm.
*/
const CONNECTION_DELAY_INTERVAL_MS = 250;
export class PickFirstLoadBalancingConfig implements LoadBalancingConfig {
getLoadBalancerName(): string {
return TYPE_NAME;
}
constructor() {}
toJsonObject(): object {
return {
[TYPE_NAME]: {},
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static createFromJson(obj: any) {
return new PickFirstLoadBalancingConfig();
}
}
/**
* Picker for a `PickFirstLoadBalancer` in the READY state. Always returns the
* picked subchannel.
*/
class PickFirstPicker implements Picker {
constructor(private subchannel: Subchannel) {}
pick(pickArgs: PickArgs): CompletePickResult {
return {
pickResultType: PickResultType.COMPLETE,
subchannel: this.subchannel,
status: null,
extraFilterFactories: [],
onCallStarted: null,
};
}
}
interface ConnectivityStateCounts {
[ConnectivityState.CONNECTING]: number;
[ConnectivityState.IDLE]: number;
[ConnectivityState.READY]: number;
[ConnectivityState.SHUTDOWN]: number;
[ConnectivityState.TRANSIENT_FAILURE]: number;
}
export class PickFirstLoadBalancer implements LoadBalancer {
/**
* The list of backend addresses most recently passed to `updateAddressList`.
*/
private latestAddressList: SubchannelAddress[] = [];
/**
* The list of subchannels this load balancer is currently attempting to
* connect to.
*/
private subchannels: Subchannel[] = [];
/**
* The current connectivity state of the load balancer.
*/
private currentState: ConnectivityState = ConnectivityState.IDLE;
/**
* The index within the `subchannels` array of the subchannel with the most
* recently started connection attempt.
*/
private currentSubchannelIndex = 0;
private subchannelStateCounts: ConnectivityStateCounts;
/**
* The currently picked subchannel used for making calls. Populated if
* and only if the load balancer's current state is READY. In that case,
* the subchannel's current state is also READY.
*/
private currentPick: Subchannel | null = null;
/**
* Listener callback attached to each subchannel in the `subchannels` list
* while establishing a connection.
*/
private subchannelStateListener: ConnectivityStateListener;
/**
* Listener callback attached to the current picked subchannel.
*/
private pickedSubchannelStateListener: ConnectivityStateListener;
/**
* Timer reference for the timer tracking when to start
*/
private connectionDelayTimeout: NodeJS.Timeout;
private triedAllSubchannels = false;
/**
* 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
* request.
* @param channelControlHelper `ChannelControlHelper` instance provided by
* this load balancer's owner.
*/
constructor(private readonly channelControlHelper: ChannelControlHelper) {
this.subchannelStateCounts = {
[ConnectivityState.CONNECTING]: 0,
[ConnectivityState.IDLE]: 0,
[ConnectivityState.READY]: 0,
[ConnectivityState.SHUTDOWN]: 0,
[ConnectivityState.TRANSIENT_FAILURE]: 0,
};
this.subchannelStateListener = (
subchannel: Subchannel,
previousState: ConnectivityState,
newState: ConnectivityState
) => {
this.subchannelStateCounts[previousState] -= 1;
this.subchannelStateCounts[newState] += 1;
/* If the subchannel we most recently attempted to start connecting
* to goes into TRANSIENT_FAILURE, immediately try to start
* connecting to the next one instead of waiting for the connection
* delay timer. */
if (
subchannel === this.subchannels[this.currentSubchannelIndex] &&
newState === ConnectivityState.TRANSIENT_FAILURE
) {
this.startNextSubchannelConnecting();
}
if (newState === ConnectivityState.READY) {
this.pickSubchannel(subchannel);
return;
} else {
if (
this.triedAllSubchannels &&
this.subchannelStateCounts[ConnectivityState.IDLE] ===
this.subchannels.length
) {
/* If all of the subchannels are IDLE we should go back to a
* basic IDLE state where there is no subchannel list to avoid
* holding unused resources */
this.resetSubchannelList();
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
return;
}
if (this.currentPick === null) {
if (this.triedAllSubchannels) {
let newLBState: ConnectivityState;
if (this.subchannelStateCounts[ConnectivityState.CONNECTING] > 0) {
newLBState = ConnectivityState.CONNECTING;
} else if (
this.subchannelStateCounts[ConnectivityState.TRANSIENT_FAILURE] >
0
) {
newLBState = ConnectivityState.TRANSIENT_FAILURE;
} else {
newLBState = ConnectivityState.IDLE;
}
if (newLBState !== this.currentState) {
if (newLBState === ConnectivityState.TRANSIENT_FAILURE) {
this.updateState(newLBState, new UnavailablePicker());
} else {
this.updateState(newLBState, new QueuePicker(this));
}
}
} else {
this.updateState(
ConnectivityState.CONNECTING,
new QueuePicker(this)
);
}
}
}
};
this.pickedSubchannelStateListener = (
subchannel: Subchannel,
previousState: ConnectivityState,
newState: ConnectivityState
) => {
if (newState !== ConnectivityState.READY) {
this.currentPick = null;
subchannel.unref();
subchannel.removeConnectivityStateListener(
this.pickedSubchannelStateListener
);
this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef());
if (this.subchannels.length > 0) {
if (this.triedAllSubchannels) {
let newLBState: ConnectivityState;
if (this.subchannelStateCounts[ConnectivityState.CONNECTING] > 0) {
newLBState = ConnectivityState.CONNECTING;
} else if (
this.subchannelStateCounts[ConnectivityState.TRANSIENT_FAILURE] >
0
) {
newLBState = ConnectivityState.TRANSIENT_FAILURE;
} else {
newLBState = ConnectivityState.IDLE;
}
if (newLBState === ConnectivityState.TRANSIENT_FAILURE) {
this.updateState(newLBState, new UnavailablePicker());
} else {
this.updateState(newLBState, new QueuePicker(this));
}
} else {
this.updateState(
ConnectivityState.CONNECTING,
new QueuePicker(this)
);
}
} else {
/* We don't need to backoff here because this only happens if a
* subchannel successfully connects then disconnects, so it will not
* create a loop of attempting to connect to an unreachable backend
*/
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
}
}
};
this.connectionDelayTimeout = setTimeout(() => {}, 0);
clearTimeout(this.connectionDelayTimeout);
}
private startNextSubchannelConnecting() {
if (this.triedAllSubchannels) {
return;
}
for (const [index, subchannel] of this.subchannels.entries()) {
if (index > this.currentSubchannelIndex) {
const subchannelState = subchannel.getConnectivityState();
if (
subchannelState === ConnectivityState.IDLE ||
subchannelState === ConnectivityState.CONNECTING
) {
this.startConnecting(index);
return;
}
}
}
this.triedAllSubchannels = true;
}
/**
* Have a single subchannel in the `subchannels` list start connecting.
* @param subchannelIndex The index into the `subchannels` list.
*/
private startConnecting(subchannelIndex: number) {
clearTimeout(this.connectionDelayTimeout);
this.currentSubchannelIndex = subchannelIndex;
if (
this.subchannels[subchannelIndex].getConnectivityState() ===
ConnectivityState.IDLE
) {
trace(
'Start connecting to subchannel with address ' +
this.subchannels[subchannelIndex].getAddress()
);
process.nextTick(() => {
this.subchannels[subchannelIndex].startConnecting();
});
}
this.connectionDelayTimeout = setTimeout(() => {
this.startNextSubchannelConnecting();
}, CONNECTION_DELAY_INTERVAL_MS);
}
private pickSubchannel(subchannel: Subchannel) {
trace('Pick subchannel with address ' + subchannel.getAddress());
if (this.currentPick !== null) {
this.currentPick.unref();
this.currentPick.removeConnectivityStateListener(
this.pickedSubchannelStateListener
);
}
this.currentPick = subchannel;
this.updateState(ConnectivityState.READY, new PickFirstPicker(subchannel));
subchannel.addConnectivityStateListener(this.pickedSubchannelStateListener);
subchannel.ref();
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef());
this.resetSubchannelList();
clearTimeout(this.connectionDelayTimeout);
}
private updateState(newState: ConnectivityState, picker: Picker) {
trace(
ConnectivityState[this.currentState] +
' -> ' +
ConnectivityState[newState]
);
this.currentState = newState;
this.channelControlHelper.updateState(newState, picker);
}
private resetSubchannelList() {
for (const subchannel of this.subchannels) {
subchannel.removeConnectivityStateListener(this.subchannelStateListener);
subchannel.unref();
this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef());
}
this.currentSubchannelIndex = 0;
this.subchannelStateCounts = {
[ConnectivityState.CONNECTING]: 0,
[ConnectivityState.IDLE]: 0,
[ConnectivityState.READY]: 0,
[ConnectivityState.SHUTDOWN]: 0,
[ConnectivityState.TRANSIENT_FAILURE]: 0,
};
this.subchannels = [];
this.triedAllSubchannels = false;
}
/**
* Start connecting to the address list most recently passed to
* `updateAddressList`.
*/
private connectToAddressList(): void {
this.resetSubchannelList();
trace(
'Connect to address list ' +
this.latestAddressList.map((address) =>
subchannelAddressToString(address)
)
);
this.subchannels = this.latestAddressList.map((address) =>
this.channelControlHelper.createSubchannel(address, {})
);
for (const subchannel of this.subchannels) {
subchannel.ref();
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef());
}
for (const subchannel of this.subchannels) {
subchannel.addConnectivityStateListener(this.subchannelStateListener);
this.subchannelStateCounts[subchannel.getConnectivityState()] += 1;
if (subchannel.getConnectivityState() === ConnectivityState.READY) {
this.pickSubchannel(subchannel);
this.resetSubchannelList();
return;
}
}
for (const [index, subchannel] of this.subchannels.entries()) {
const subchannelState = subchannel.getConnectivityState();
if (
subchannelState === ConnectivityState.IDLE ||
subchannelState === ConnectivityState.CONNECTING
) {
this.startConnecting(index);
if (this.currentPick === null) {
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
}
return;
}
}
// If the code reaches this point, every subchannel must be in TRANSIENT_FAILURE
if (this.currentPick === null) {
this.updateState(
ConnectivityState.TRANSIENT_FAILURE,
new UnavailablePicker()
);
}
}
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig
): void {
// lbConfig has no useful information for pick first load balancing
/* To avoid unnecessary churn, we only do something with this address list
* if we're not currently trying to establish a connection, or if the new
* address list is different from the existing one */
if (
this.subchannels.length === 0 ||
!this.latestAddressList.every(
(value, index) => addressList[index] === value
)
) {
this.latestAddressList = addressList;
this.connectToAddressList();
}
}
exitIdle() {
for (const subchannel of this.subchannels) {
subchannel.startConnecting();
}
if (this.currentState === ConnectivityState.IDLE) {
if (this.latestAddressList.length > 0) {
this.connectToAddressList();
}
}
if (
this.currentState === ConnectivityState.IDLE ||
this.triedAllSubchannels
) {
this.channelControlHelper.requestReresolution();
}
}
resetBackoff() {
/* The pick first load balancer does not have a connection backoff, so this
* does nothing */
}
destroy() {
this.resetSubchannelList();
if (this.currentPick !== null) {
this.currentPick.unref();
this.currentPick.removeConnectivityStateListener(
this.pickedSubchannelStateListener
);
this.channelControlHelper.removeChannelzChild(this.currentPick.getChannelzRef());
}
}
getTypeName(): string {
return TYPE_NAME;
}
}
export function setup(): void {
registerLoadBalancerType(
TYPE_NAME,
PickFirstLoadBalancer,
PickFirstLoadBalancingConfig
);
registerDefaultLoadBalancerType(TYPE_NAME);
}
+257
View File
@@ -0,0 +1,257 @@
/*
* 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 {
LoadBalancer,
ChannelControlHelper,
LoadBalancingConfig,
registerLoadBalancerType,
} from './load-balancer';
import { ConnectivityState } from './connectivity-state';
import {
QueuePicker,
Picker,
PickArgs,
CompletePickResult,
PickResultType,
UnavailablePicker,
} from './picker';
import { Subchannel, ConnectivityStateListener } from './subchannel';
import {
SubchannelAddress,
subchannelAddressToString,
} from './subchannel-address';
import * as logging from './logging';
import { LogVerbosity } from './constants';
const TRACER_NAME = 'round_robin';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
const TYPE_NAME = 'round_robin';
class RoundRobinLoadBalancingConfig implements LoadBalancingConfig {
getLoadBalancerName(): string {
return TYPE_NAME;
}
constructor() {}
toJsonObject(): object {
return {
[TYPE_NAME]: {},
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static createFromJson(obj: any) {
return new RoundRobinLoadBalancingConfig();
}
}
class RoundRobinPicker implements Picker {
constructor(
private readonly subchannelList: Subchannel[],
private nextIndex = 0
) {}
pick(pickArgs: PickArgs): CompletePickResult {
const pickedSubchannel = this.subchannelList[this.nextIndex];
this.nextIndex = (this.nextIndex + 1) % this.subchannelList.length;
return {
pickResultType: PickResultType.COMPLETE,
subchannel: pickedSubchannel,
status: null,
extraFilterFactories: [],
onCallStarted: null,
};
}
/**
* Check what the next subchannel returned would be. Used by the load
* balancer implementation to preserve this part of the picker state if
* possible when a subchannel connects or disconnects.
*/
peekNextSubchannel(): Subchannel {
return this.subchannelList[this.nextIndex];
}
}
interface ConnectivityStateCounts {
[ConnectivityState.CONNECTING]: number;
[ConnectivityState.IDLE]: number;
[ConnectivityState.READY]: number;
[ConnectivityState.SHUTDOWN]: number;
[ConnectivityState.TRANSIENT_FAILURE]: number;
}
export class RoundRobinLoadBalancer implements LoadBalancer {
private subchannels: Subchannel[] = [];
private currentState: ConnectivityState = ConnectivityState.IDLE;
private subchannelStateListener: ConnectivityStateListener;
private subchannelStateCounts: ConnectivityStateCounts;
private currentReadyPicker: RoundRobinPicker | null = null;
constructor(private readonly channelControlHelper: ChannelControlHelper) {
this.subchannelStateCounts = {
[ConnectivityState.CONNECTING]: 0,
[ConnectivityState.IDLE]: 0,
[ConnectivityState.READY]: 0,
[ConnectivityState.SHUTDOWN]: 0,
[ConnectivityState.TRANSIENT_FAILURE]: 0,
};
this.subchannelStateListener = (
subchannel: Subchannel,
previousState: ConnectivityState,
newState: ConnectivityState
) => {
this.subchannelStateCounts[previousState] -= 1;
this.subchannelStateCounts[newState] += 1;
this.calculateAndUpdateState();
if (
newState === ConnectivityState.TRANSIENT_FAILURE ||
newState === ConnectivityState.IDLE
) {
this.channelControlHelper.requestReresolution();
subchannel.startConnecting();
}
};
}
private calculateAndUpdateState() {
if (this.subchannelStateCounts[ConnectivityState.READY] > 0) {
const readySubchannels = this.subchannels.filter(
(subchannel) =>
subchannel.getConnectivityState() === ConnectivityState.READY
);
let index = 0;
if (this.currentReadyPicker !== null) {
index = readySubchannels.indexOf(
this.currentReadyPicker.peekNextSubchannel()
);
if (index < 0) {
index = 0;
}
}
this.updateState(
ConnectivityState.READY,
new RoundRobinPicker(readySubchannels, index)
);
} else if (this.subchannelStateCounts[ConnectivityState.CONNECTING] > 0) {
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
} else if (
this.subchannelStateCounts[ConnectivityState.TRANSIENT_FAILURE] > 0
) {
this.updateState(
ConnectivityState.TRANSIENT_FAILURE,
new UnavailablePicker()
);
} else {
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
}
}
private updateState(newState: ConnectivityState, picker: Picker) {
trace(
ConnectivityState[this.currentState] +
' -> ' +
ConnectivityState[newState]
);
if (newState === ConnectivityState.READY) {
this.currentReadyPicker = picker as RoundRobinPicker;
} else {
this.currentReadyPicker = null;
}
this.currentState = newState;
this.channelControlHelper.updateState(newState, picker);
}
private resetSubchannelList() {
for (const subchannel of this.subchannels) {
subchannel.removeConnectivityStateListener(this.subchannelStateListener);
subchannel.unref();
this.channelControlHelper.removeChannelzChild(subchannel.getChannelzRef());
}
this.subchannelStateCounts = {
[ConnectivityState.CONNECTING]: 0,
[ConnectivityState.IDLE]: 0,
[ConnectivityState.READY]: 0,
[ConnectivityState.SHUTDOWN]: 0,
[ConnectivityState.TRANSIENT_FAILURE]: 0,
};
this.subchannels = [];
}
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig
): void {
this.resetSubchannelList();
trace(
'Connect to address list ' +
addressList.map((address) => subchannelAddressToString(address))
);
this.subchannels = addressList.map((address) =>
this.channelControlHelper.createSubchannel(address, {})
);
for (const subchannel of this.subchannels) {
subchannel.ref();
subchannel.addConnectivityStateListener(this.subchannelStateListener);
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef());
const subchannelState = subchannel.getConnectivityState();
this.subchannelStateCounts[subchannelState] += 1;
if (
subchannelState === ConnectivityState.IDLE ||
subchannelState === ConnectivityState.TRANSIENT_FAILURE
) {
subchannel.startConnecting();
}
}
this.calculateAndUpdateState();
}
exitIdle(): void {
for (const subchannel of this.subchannels) {
subchannel.startConnecting();
}
}
resetBackoff(): void {
/* The pick first load balancer does not have a connection backoff, so this
* does nothing */
}
destroy(): void {
this.resetSubchannelList();
}
getTypeName(): string {
return TYPE_NAME;
}
}
export function setup() {
registerLoadBalancerType(
TYPE_NAME,
RoundRobinLoadBalancer,
RoundRobinLoadBalancingConfig
);
}
+218
View File
@@ -0,0 +1,218 @@
/*
* 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 { ChannelOptions } from './channel-options';
import { Subchannel } from './subchannel';
import { SubchannelAddress } from './subchannel-address';
import { ConnectivityState } from './connectivity-state';
import { Picker } from './picker';
import { ChannelRef, SubchannelRef } from './channelz';
/**
* A collection of functions associated with a channel that a load balancer
* can call as necessary.
*/
export interface ChannelControlHelper {
/**
* Returns a subchannel connected to the specified address.
* @param subchannelAddress The address to connect to
* @param subchannelArgs Extra channel arguments specified by the load balancer
*/
createSubchannel(
subchannelAddress: SubchannelAddress,
subchannelArgs: ChannelOptions
): Subchannel;
/**
* Passes a new subchannel picker up to the channel. This is called if either
* the connectivity state changes or if a different picker is needed for any
* other reason.
* @param connectivityState New connectivity state
* @param picker New picker
*/
updateState(connectivityState: ConnectivityState, picker: Picker): void;
/**
* Request new data from the resolver.
*/
requestReresolution(): void;
addChannelzChild(child: ChannelRef | SubchannelRef): void;
removeChannelzChild(child: ChannelRef | SubchannelRef): void;
}
/**
* Create a child ChannelControlHelper that overrides some methods of the
* parent while letting others pass through to the parent unmodified. This
* allows other code to create these children without needing to know about
* all of the methods to be passed through.
* @param parent
* @param overrides
*/
export function createChildChannelControlHelper(parent: ChannelControlHelper, overrides: Partial<ChannelControlHelper>): ChannelControlHelper {
return {
createSubchannel: overrides.createSubchannel?.bind(overrides) ?? parent.createSubchannel.bind(parent),
updateState: overrides.updateState?.bind(overrides) ?? parent.updateState.bind(parent),
requestReresolution: overrides.requestReresolution?.bind(overrides) ?? parent.requestReresolution.bind(parent),
addChannelzChild: overrides.addChannelzChild?.bind(overrides) ?? parent.addChannelzChild.bind(parent),
removeChannelzChild: overrides.removeChannelzChild?.bind(overrides) ?? parent.removeChannelzChild.bind(parent)
};
}
/**
* Tracks one or more connected subchannels and determines which subchannel
* each request should use.
*/
export interface LoadBalancer {
/**
* Gives the load balancer a new list of addresses to start connecting to.
* The load balancer will start establishing connections with the new list,
* but will continue using any existing connections until the new connections
* are established
* @param addressList The new list of addresses to connect to
* @param lbConfig The load balancing config object from the service config,
* if one was provided
*/
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig,
attributes: { [key: string]: unknown }
): void;
/**
* If the load balancer is currently in the IDLE state, start connecting.
*/
exitIdle(): void;
/**
* If the load balancer is currently in the CONNECTING or TRANSIENT_FAILURE
* state, reset the current connection backoff timeout to its base value and
* transition to CONNECTING if in TRANSIENT_FAILURE.
*/
resetBackoff(): void;
/**
* The load balancer unrefs all of its subchannels and stops calling methods
* of its channel control helper.
*/
destroy(): void;
/**
* Get the type name for this load balancer type. Must be constant across an
* entire load balancer implementation class and must match the name that the
* balancer implementation class was registered with.
*/
getTypeName(): string;
}
export interface LoadBalancerConstructor {
new (channelControlHelper: ChannelControlHelper): LoadBalancer;
}
export interface LoadBalancingConfig {
getLoadBalancerName(): string;
toJsonObject(): object;
}
export interface LoadBalancingConfigConstructor {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any): LoadBalancingConfig;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createFromJson(obj: any): LoadBalancingConfig;
}
const registeredLoadBalancerTypes: {
[name: string]: {
LoadBalancer: LoadBalancerConstructor;
LoadBalancingConfig: LoadBalancingConfigConstructor;
};
} = {};
let defaultLoadBalancerType: string | null = null;
export function registerLoadBalancerType(
typeName: string,
loadBalancerType: LoadBalancerConstructor,
loadBalancingConfigType: LoadBalancingConfigConstructor
) {
registeredLoadBalancerTypes[typeName] = {
LoadBalancer: loadBalancerType,
LoadBalancingConfig: loadBalancingConfigType,
};
}
export function registerDefaultLoadBalancerType(typeName: string) {
defaultLoadBalancerType = typeName;
}
export function createLoadBalancer(
config: LoadBalancingConfig,
channelControlHelper: ChannelControlHelper
): LoadBalancer | null {
const typeName = config.getLoadBalancerName();
if (typeName in registeredLoadBalancerTypes) {
return new registeredLoadBalancerTypes[typeName].LoadBalancer(
channelControlHelper
);
} else {
return null;
}
}
export function isLoadBalancerNameRegistered(typeName: string): boolean {
return typeName in registeredLoadBalancerTypes;
}
export function getFirstUsableConfig(
configs: LoadBalancingConfig[],
fallbackTodefault?: true
): LoadBalancingConfig;
export function getFirstUsableConfig(
configs: LoadBalancingConfig[],
fallbackTodefault = false
): LoadBalancingConfig | null {
for (const config of configs) {
if (config.getLoadBalancerName() in registeredLoadBalancerTypes) {
return config;
}
}
if (fallbackTodefault) {
if (defaultLoadBalancerType) {
return new registeredLoadBalancerTypes[
defaultLoadBalancerType
]!.LoadBalancingConfig();
} else {
return null;
}
} else {
return null;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function validateLoadBalancingConfig(obj: any): LoadBalancingConfig {
if (!(obj !== null && typeof obj === 'object')) {
throw new Error('Load balancing config must be an object');
}
const keys = Object.keys(obj);
if (keys.length !== 1) {
throw new Error(
'Provided load balancing config has multiple conflicting entries'
);
}
const typeName = keys[0];
if (typeName in registeredLoadBalancerTypes) {
return registeredLoadBalancerTypes[
typeName
].LoadBalancingConfig.createFromJson(obj[typeName]);
} else {
throw new Error(`Unrecognized load balancing config name ${typeName}`);
}
}
+117
View File
@@ -0,0 +1,117 @@
/*
* 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 { LogVerbosity } from './constants';
const DEFAULT_LOGGER: Partial<Console> = {
error: (message?: any, ...optionalParams: any[]) => {
console.error('E ' + message, ...optionalParams);
},
info: (message?: any, ...optionalParams: any[]) => {
console.error('I ' + message, ...optionalParams);
},
debug: (message?: any, ...optionalParams: any[]) => {
console.error('D ' + message, ...optionalParams);
},
}
let _logger: Partial<Console> = DEFAULT_LOGGER;
let _logVerbosity: LogVerbosity = LogVerbosity.ERROR;
const verbosityString =
process.env.GRPC_NODE_VERBOSITY ?? process.env.GRPC_VERBOSITY ?? '';
switch (verbosityString.toUpperCase()) {
case 'DEBUG':
_logVerbosity = LogVerbosity.DEBUG;
break;
case 'INFO':
_logVerbosity = LogVerbosity.INFO;
break;
case 'ERROR':
_logVerbosity = LogVerbosity.ERROR;
break;
case 'NONE':
_logVerbosity = LogVerbosity.NONE;
break;
default:
// Ignore any other values
}
export const getLogger = (): Partial<Console> => {
return _logger;
};
export const setLogger = (logger: Partial<Console>): void => {
_logger = logger;
};
export const setLoggerVerbosity = (verbosity: LogVerbosity): void => {
_logVerbosity = verbosity;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const log = (severity: LogVerbosity, ...args: any[]): void => {
let logFunction: typeof DEFAULT_LOGGER.error;
if (severity >= _logVerbosity) {
switch (severity) {
case LogVerbosity.DEBUG:
logFunction = _logger.debug;
break;
case LogVerbosity.INFO:
logFunction = _logger.info;
break;
case LogVerbosity.ERROR:
logFunction = _logger.error;
break;
}
/* Fall back to _logger.error when other methods are not available for
* compatiblity with older behavior that always logged to _logger.error */
if (!logFunction) {
logFunction = _logger.error;
}
if (logFunction) {
logFunction.bind(_logger)(...args);
}
}
};
const tracersString =
process.env.GRPC_NODE_TRACE ?? process.env.GRPC_TRACE ?? '';
const enabledTracers = new Set<string>();
const disabledTracers = new Set<string>();
for (const tracerName of tracersString.split(',')) {
if (tracerName.startsWith('-')) {
disabledTracers.add(tracerName.substring(1));
} else {
enabledTracers.add(tracerName);
}
}
const allEnabled = enabledTracers.has('all');
export function trace(
severity: LogVerbosity,
tracer: string,
text: string
): void {
if (
!disabledTracers.has(tracer) &&
(allEnabled || enabledTracers.has(tracer))
) {
log(severity, new Date().toISOString() + ' | ' + tracer + ' | ' + text);
}
}
+235
View File
@@ -0,0 +1,235 @@
/*
* 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 { ChannelOptions } from './channel-options';
import { Client } from './client';
import { UntypedServiceImplementation } from './server';
export interface Serialize<T> {
(value: T): Buffer;
}
export interface Deserialize<T> {
(bytes: Buffer): T;
}
export interface ClientMethodDefinition<RequestType, ResponseType> {
path: string;
requestStream: boolean;
responseStream: boolean;
requestSerialize: Serialize<RequestType>;
responseDeserialize: Deserialize<ResponseType>;
originalName?: string;
}
export interface ServerMethodDefinition<RequestType, ResponseType> {
path: string;
requestStream: boolean;
responseStream: boolean;
responseSerialize: Serialize<ResponseType>;
requestDeserialize: Deserialize<RequestType>;
originalName?: string;
}
export interface MethodDefinition<RequestType, ResponseType>
extends ClientMethodDefinition<RequestType, ResponseType>,
ServerMethodDefinition<RequestType, ResponseType> {}
/* eslint-disable @typescript-eslint/no-explicit-any */
export type ServiceDefinition<
ImplementationType = UntypedServiceImplementation
> = {
readonly [index in keyof ImplementationType]: MethodDefinition<any, any>;
};
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface ProtobufTypeDefinition {
format: string;
type: object;
fileDescriptorProtos: Buffer[];
}
export interface PackageDefinition {
[index: string]: ServiceDefinition | ProtobufTypeDefinition;
}
/**
* Map with short names for each of the requester maker functions. Used in
* makeClientConstructor
* @private
*/
const requesterFuncs = {
unary: Client.prototype.makeUnaryRequest,
server_stream: Client.prototype.makeServerStreamRequest,
client_stream: Client.prototype.makeClientStreamRequest,
bidi: Client.prototype.makeBidiStreamRequest,
};
export interface ServiceClient extends Client {
[methodName: string]: Function;
}
export interface ServiceClientConstructor {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ChannelOptions>
): ServiceClient;
service: ServiceDefinition;
}
/**
* Returns true, if given key is included in the blacklisted
* keys.
* @param key key for check, string.
*/
function isPrototypePolluted(key: string): boolean {
return ['__proto__', 'prototype', 'constructor'].includes(key);
}
/**
* Creates a constructor for a client with the given methods, as specified in
* the methods argument. The resulting class will have an instance method for
* each method in the service, which is a partial application of one of the
* [Client]{@link grpc.Client} request methods, depending on `requestSerialize`
* and `responseSerialize`, with the `method`, `serialize`, and `deserialize`
* arguments predefined.
* @param methods An object mapping method names to
* method attributes
* @param serviceName The fully qualified name of the service
* @param classOptions An options object.
* @return New client constructor, which is a subclass of
* {@link grpc.Client}, and has the same arguments as that constructor.
*/
export function makeClientConstructor(
methods: ServiceDefinition,
serviceName: string,
classOptions?: {}
): ServiceClientConstructor {
if (!classOptions) {
classOptions = {};
}
class ServiceClientImpl extends Client implements ServiceClient {
static service: ServiceDefinition;
[methodName: string]: Function;
}
Object.keys(methods).forEach((name) => {
if (isPrototypePolluted(name)) {
return;
}
const attrs = methods[name];
let methodType: keyof typeof requesterFuncs;
// TODO(murgatroid99): Verify that we don't need this anymore
if (typeof name === 'string' && name.charAt(0) === '$') {
throw new Error('Method names cannot start with $');
}
if (attrs.requestStream) {
if (attrs.responseStream) {
methodType = 'bidi';
} else {
methodType = 'client_stream';
}
} else {
if (attrs.responseStream) {
methodType = 'server_stream';
} else {
methodType = 'unary';
}
}
const serialize = attrs.requestSerialize;
const deserialize = attrs.responseDeserialize;
const methodFunc = partial(
requesterFuncs[methodType],
attrs.path,
serialize,
deserialize
);
ServiceClientImpl.prototype[name] = methodFunc;
// Associate all provided attributes with the method
Object.assign(ServiceClientImpl.prototype[name], attrs);
if (attrs.originalName && !isPrototypePolluted(attrs.originalName)) {
ServiceClientImpl.prototype[attrs.originalName] =
ServiceClientImpl.prototype[name];
}
});
ServiceClientImpl.service = methods;
return ServiceClientImpl;
}
function partial(
fn: Function,
path: string,
serialize: Function,
deserialize: Function
): Function {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (this: any, ...args: any[]) {
return fn.call(this, path, serialize, deserialize, ...args);
};
}
export interface GrpcObject {
[index: string]:
| GrpcObject
| ServiceClientConstructor
| ProtobufTypeDefinition;
}
function isProtobufTypeDefinition(
obj: ServiceDefinition | ProtobufTypeDefinition
): obj is ProtobufTypeDefinition {
return 'format' in obj;
}
/**
* Load a gRPC package definition as a gRPC object hierarchy.
* @param packageDef The package definition object.
* @return The resulting gRPC object.
*/
export function loadPackageDefinition(
packageDef: PackageDefinition
): GrpcObject {
const result: GrpcObject = {};
for (const serviceFqn in packageDef) {
if (Object.prototype.hasOwnProperty.call(packageDef, serviceFqn)) {
const service = packageDef[serviceFqn];
const nameComponents = serviceFqn.split('.');
if (nameComponents.some((comp: string) => isPrototypePolluted(comp))) {
continue;
}
const serviceName = nameComponents[nameComponents.length - 1];
let current = result;
for (const packageName of nameComponents.slice(0, -1)) {
if (!current[packageName]) {
current[packageName] = {};
}
current = current[packageName] as GrpcObject;
}
if (isProtobufTypeDefinition(service)) {
current[serviceName] = service;
} else {
current[serviceName] = makeClientConstructor(service, serviceName, {});
}
}
}
return result;
}
+89
View File
@@ -0,0 +1,89 @@
/*
* Copyright 2020 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 { BaseFilter, Filter, FilterFactory } from './filter';
import { Call, WriteObject } from './call-stream';
import {
Status,
DEFAULT_MAX_SEND_MESSAGE_LENGTH,
DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH,
} from './constants';
import { ChannelOptions } from './channel-options';
export class MaxMessageSizeFilter extends BaseFilter implements Filter {
private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH;
private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH;
constructor(
private readonly options: ChannelOptions,
private readonly callStream: Call
) {
super();
if ('grpc.max_send_message_length' in options) {
this.maxSendMessageSize = options['grpc.max_send_message_length']!;
}
if ('grpc.max_receive_message_length' in options) {
this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!;
}
}
async sendMessage(message: Promise<WriteObject>): Promise<WriteObject> {
/* A configured size of -1 means that there is no limit, so skip the check
* entirely */
if (this.maxSendMessageSize === -1) {
return message;
} else {
const concreteMessage = await message;
if (concreteMessage.message.length > this.maxSendMessageSize) {
this.callStream.cancelWithStatus(
Status.RESOURCE_EXHAUSTED,
`Sent message larger than max (${concreteMessage.message.length} vs. ${this.maxSendMessageSize})`
);
return Promise.reject<WriteObject>('Message too large');
} else {
return concreteMessage;
}
}
}
async receiveMessage(message: Promise<Buffer>): Promise<Buffer> {
/* A configured size of -1 means that there is no limit, so skip the check
* entirely */
if (this.maxReceiveMessageSize === -1) {
return message;
} else {
const concreteMessage = await message;
if (concreteMessage.length > this.maxReceiveMessageSize) {
this.callStream.cancelWithStatus(
Status.RESOURCE_EXHAUSTED,
`Received message larger than max (${concreteMessage.length} vs. ${this.maxReceiveMessageSize})`
);
return Promise.reject<Buffer>('Message too large');
} else {
return concreteMessage;
}
}
}
}
export class MaxMessageSizeFilterFactory
implements FilterFactory<MaxMessageSizeFilter> {
constructor(private readonly options: ChannelOptions) {}
createFilter(callStream: Call): MaxMessageSizeFilter {
return new MaxMessageSizeFilter(this.options, callStream);
}
}
+303
View File
@@ -0,0 +1,303 @@
/*
* 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 * as http2 from 'http2';
import { log } from './logging';
import { LogVerbosity } from './constants';
const LEGAL_KEY_REGEX = /^[0-9a-z_.-]+$/;
const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/;
export type MetadataValue = string | Buffer;
export type MetadataObject = Map<string, MetadataValue[]>;
function isLegalKey(key: string): boolean {
return LEGAL_KEY_REGEX.test(key);
}
function isLegalNonBinaryValue(value: string): boolean {
return LEGAL_NON_BINARY_VALUE_REGEX.test(value);
}
function isBinaryKey(key: string): boolean {
return key.endsWith('-bin');
}
function isCustomMetadata(key: string): boolean {
return !key.startsWith('grpc-');
}
function normalizeKey(key: string): string {
return key.toLowerCase();
}
function validate(key: string, value?: MetadataValue): void {
if (!isLegalKey(key)) {
throw new Error('Metadata key "' + key + '" contains illegal characters');
}
if (value !== null && value !== undefined) {
if (isBinaryKey(key)) {
if (!(value instanceof Buffer)) {
throw new Error("keys that end with '-bin' must have Buffer values");
}
} else {
if (value instanceof Buffer) {
throw new Error(
"keys that don't end with '-bin' must have String values"
);
}
if (!isLegalNonBinaryValue(value)) {
throw new Error(
'Metadata string value "' + value + '" contains illegal characters'
);
}
}
}
}
export interface MetadataOptions {
/* Signal that the request is idempotent. Defaults to false */
idempotentRequest?: boolean;
/* Signal that the call should not return UNAVAILABLE before it has
* started. Defaults to false. */
waitForReady?: boolean;
/* Signal that the call is cacheable. GRPC is free to use GET verb.
* Defaults to false */
cacheableRequest?: boolean;
/* Signal that the initial metadata should be corked. Defaults to false. */
corked?: boolean;
}
/**
* A class for storing metadata. Keys are normalized to lowercase ASCII.
*/
export class Metadata {
protected internalRepr: MetadataObject = new Map<string, MetadataValue[]>();
private options: MetadataOptions;
constructor(options?: MetadataOptions) {
if (options === undefined) {
this.options = {};
} else {
this.options = options;
}
}
/**
* Sets the given value for the given key by replacing any other values
* associated with that key. Normalizes the key.
* @param key The key to whose value should be set.
* @param value The value to set. Must be a buffer if and only
* if the normalized key ends with '-bin'.
*/
set(key: string, value: MetadataValue): void {
key = normalizeKey(key);
validate(key, value);
this.internalRepr.set(key, [value]);
}
/**
* Adds the given value for the given key by appending to a list of previous
* values associated with that key. Normalizes the key.
* @param key The key for which a new value should be appended.
* @param value The value to add. Must be a buffer if and only
* if the normalized key ends with '-bin'.
*/
add(key: string, value: MetadataValue): void {
key = normalizeKey(key);
validate(key, value);
const existingValue: MetadataValue[] | undefined = this.internalRepr.get(
key
);
if (existingValue === undefined) {
this.internalRepr.set(key, [value]);
} else {
existingValue.push(value);
}
}
/**
* Removes the given key and any associated values. Normalizes the key.
* @param key The key whose values should be removed.
*/
remove(key: string): void {
key = normalizeKey(key);
validate(key);
this.internalRepr.delete(key);
}
/**
* Gets a list of all values associated with the key. Normalizes the key.
* @param key The key whose value should be retrieved.
* @return A list of values associated with the given key.
*/
get(key: string): MetadataValue[] {
key = normalizeKey(key);
validate(key);
return this.internalRepr.get(key) || [];
}
/**
* Gets a plain object mapping each key to the first value associated with it.
* This reflects the most common way that people will want to see metadata.
* @return A key/value mapping of the metadata.
*/
getMap(): { [key: string]: MetadataValue } {
const result: { [key: string]: MetadataValue } = {};
this.internalRepr.forEach((values, key) => {
if (values.length > 0) {
const v = values[0];
result[key] = v instanceof Buffer ? v.slice() : v;
}
});
return result;
}
/**
* Clones the metadata object.
* @return The newly cloned object.
*/
clone(): Metadata {
const newMetadata = new Metadata(this.options);
const newInternalRepr = newMetadata.internalRepr;
this.internalRepr.forEach((value, key) => {
const clonedValue: MetadataValue[] = value.map((v) => {
if (v instanceof Buffer) {
return Buffer.from(v);
} else {
return v;
}
});
newInternalRepr.set(key, clonedValue);
});
return newMetadata;
}
/**
* Merges all key-value pairs from a given Metadata object into this one.
* If both this object and the given object have values in the same key,
* values from the other Metadata object will be appended to this object's
* values.
* @param other A Metadata object.
*/
merge(other: Metadata): void {
other.internalRepr.forEach((values, key) => {
const mergedValue: MetadataValue[] = (
this.internalRepr.get(key) || []
).concat(values);
this.internalRepr.set(key, mergedValue);
});
}
setOptions(options: MetadataOptions) {
this.options = options;
}
getOptions(): MetadataOptions {
return this.options;
}
/**
* Creates an OutgoingHttpHeaders object that can be used with the http2 API.
*/
toHttp2Headers(): http2.OutgoingHttpHeaders {
// NOTE: Node <8.9 formats http2 headers incorrectly.
const result: http2.OutgoingHttpHeaders = {};
this.internalRepr.forEach((values, key) => {
// We assume that the user's interaction with this object is limited to
// through its public API (i.e. keys and values are already validated).
result[key] = values.map((value) => {
if (value instanceof Buffer) {
return value.toString('base64');
} else {
return value;
}
});
});
return result;
}
// For compatibility with the other Metadata implementation
private _getCoreRepresentation() {
return this.internalRepr;
}
/**
* This modifies the behavior of JSON.stringify to show an object
* representation of the metadata map.
*/
toJSON() {
const result: { [key: string]: MetadataValue[] } = {};
for (const [key, values] of this.internalRepr.entries()) {
result[key] = values;
}
return result;
}
/**
* Returns a new Metadata object based fields in a given IncomingHttpHeaders
* object.
* @param headers An IncomingHttpHeaders object.
*/
static fromHttp2Headers(headers: http2.IncomingHttpHeaders): Metadata {
const result = new Metadata();
Object.keys(headers).forEach((key) => {
// Reserved headers (beginning with `:`) are not valid keys.
if (key.charAt(0) === ':') {
return;
}
const values = headers[key];
try {
if (isBinaryKey(key)) {
if (Array.isArray(values)) {
values.forEach((value) => {
result.add(key, Buffer.from(value, 'base64'));
});
} else if (values !== undefined) {
if (isCustomMetadata(key)) {
values.split(',').forEach((v) => {
result.add(key, Buffer.from(v.trim(), 'base64'));
});
} else {
result.add(key, Buffer.from(values, 'base64'));
}
}
} else {
if (Array.isArray(values)) {
values.forEach((value) => {
result.add(key, value);
});
} else if (values !== undefined) {
result.add(key, values);
}
}
} catch (error) {
const message = `Failed to add metadata entry ${key}: ${values}. ${error.message}. For more information see https://github.com/grpc/grpc-node/issues/1173`;
log(LogVerbosity.ERROR, message);
}
});
return result;
}
}
+65
View File
@@ -0,0 +1,65 @@
/*
* 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 { Duplex, Readable, Writable } from 'stream';
import { EmitterAugmentation1 } from './events';
/* eslint-disable @typescript-eslint/no-explicit-any */
export type WriteCallback = (error: Error | null | undefined) => void;
export interface IntermediateObjectReadable<T> extends Readable {
read(size?: number): any & T;
}
export type ObjectReadable<T> = {
read(size?: number): T;
} & EmitterAugmentation1<'data', T> &
IntermediateObjectReadable<T>;
export interface IntermediateObjectWritable<T> extends Writable {
_write(chunk: any & T, encoding: string, callback: Function): void;
write(chunk: any & T, cb?: WriteCallback): boolean;
write(chunk: any & T, encoding?: any, cb?: WriteCallback): boolean;
setDefaultEncoding(encoding: string): this;
end(): void;
end(chunk: any & T, cb?: Function): void;
end(chunk: any & T, encoding?: any, cb?: Function): void;
}
export interface ObjectWritable<T> extends IntermediateObjectWritable<T> {
_write(chunk: T, encoding: string, callback: Function): void;
write(chunk: T, cb?: Function): boolean;
write(chunk: T, encoding?: any, cb?: Function): boolean;
setDefaultEncoding(encoding: string): this;
end(): void;
end(chunk: T, cb?: Function): void;
end(chunk: T, encoding?: any, cb?: Function): void;
}
export type ObjectDuplex<T, U> = {
read(size?: number): U;
_write(chunk: T, encoding: string, callback: Function): void;
write(chunk: T, cb?: Function): boolean;
write(chunk: T, encoding?: any, cb?: Function): boolean;
end(): void;
end(chunk: T, cb?: Function): void;
end(chunk: T, encoding?: any, cb?: Function): void;
} & Duplex &
ObjectWritable<T> &
ObjectReadable<U>;
+155
View File
@@ -0,0 +1,155 @@
/*
* 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 { Subchannel } from './subchannel';
import { StatusObject } from './call-stream';
import { Metadata } from './metadata';
import { Status } from './constants';
import { LoadBalancer } from './load-balancer';
import { FilterFactory, Filter } from './filter';
export enum PickResultType {
COMPLETE,
QUEUE,
TRANSIENT_FAILURE,
DROP,
}
export interface PickResult {
pickResultType: PickResultType;
/**
* The subchannel to use as the transport for the call. Only meaningful if
* `pickResultType` is COMPLETE. If null, indicates that the call should be
* dropped.
*/
subchannel: Subchannel | null;
/**
* The status object to end the call with. Populated if and only if
* `pickResultType` is TRANSIENT_FAILURE.
*/
status: StatusObject | null;
/**
* Extra FilterFactory (can be multiple encapsulated in a FilterStackFactory)
* provided by the load balancer to be used with the call. For technical
* reasons filters from this factory will not see sendMetadata events.
*/
extraFilterFactories: FilterFactory<Filter>[];
onCallStarted: (() => void) | null;
}
export interface CompletePickResult extends PickResult {
pickResultType: PickResultType.COMPLETE;
subchannel: Subchannel | null;
status: null;
extraFilterFactories: FilterFactory<Filter>[];
onCallStarted: (() => void) | null;
}
export interface QueuePickResult extends PickResult {
pickResultType: PickResultType.QUEUE;
subchannel: null;
status: null;
extraFilterFactories: [];
onCallStarted: null;
}
export interface TransientFailurePickResult extends PickResult {
pickResultType: PickResultType.TRANSIENT_FAILURE;
subchannel: null;
status: StatusObject;
extraFilterFactories: [];
onCallStarted: null;
}
export interface DropCallPickResult extends PickResult {
pickResultType: PickResultType.DROP;
subchannel: null;
status: StatusObject;
extraFilterFactories: [];
onCallStarted: null;
}
export interface PickArgs {
metadata: Metadata;
extraPickInfo: { [key: string]: string };
}
/**
* A proxy object representing the momentary state of a load balancer. Picks
* subchannels or returns other information based on that state. Should be
* replaced every time the load balancer changes state.
*/
export interface Picker {
pick(pickArgs: PickArgs): PickResult;
}
/**
* A standard picker representing a load balancer in the TRANSIENT_FAILURE
* state. Always responds to every pick request with an UNAVAILABLE status.
*/
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(),
};
}
}
pick(pickArgs: PickArgs): TransientFailurePickResult {
return {
pickResultType: PickResultType.TRANSIENT_FAILURE,
subchannel: null,
status: this.status,
extraFilterFactories: [],
onCallStarted: null,
};
}
}
/**
* A standard picker representing a load balancer in the IDLE or CONNECTING
* state. Always responds to every pick request with a QUEUE pick result
* indicating that the pick should be tried again with the next `Picker`. Also
* reports back to the load balancer that a connection should be established
* once any pick is attempted.
*/
export class QueuePicker {
private calledExitIdle = false;
// Constructed with a load balancer. Calls exitIdle on it the first time pick is called
constructor(private loadBalancer: LoadBalancer) {}
pick(pickArgs: PickArgs): QueuePickResult {
if (!this.calledExitIdle) {
process.nextTick(() => {
this.loadBalancer.exitIdle();
});
this.calledExitIdle = true;
}
return {
pickResultType: PickResultType.QUEUE,
subchannel: null,
status: null,
extraFilterFactories: [],
onCallStarted: null,
};
}
}
+301
View File
@@ -0,0 +1,301 @@
/*
* 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 {
Resolver,
ResolverListener,
registerResolver,
registerDefaultScheme,
} from './resolver';
import * as dns from 'dns';
import * as util from 'util';
import { extractAndSelectServiceConfig, ServiceConfig } from './service-config';
import { Status } from './constants';
import { StatusObject } from './call-stream';
import { Metadata } from './metadata';
import * as logging from './logging';
import { LogVerbosity } from './constants';
import { SubchannelAddress, TcpSubchannelAddress } from './subchannel-address';
import { GrpcUri, uriToString, splitHostPort } from './uri-parser';
import { isIPv6, isIPv4 } from 'net';
import { ChannelOptions } from './channel-options';
const TRACER_NAME = 'dns_resolver';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
/**
* The default TCP port to connect to if not explicitly specified in the target.
*/
const DEFAULT_PORT = 443;
const resolveTxtPromise = util.promisify(dns.resolveTxt);
const dnsLookupPromise = util.promisify(dns.lookup);
/**
* Merge any number of arrays into a single alternating array
* @param arrays
*/
function mergeArrays<T>(...arrays: T[][]): T[] {
const result: T[] = [];
for (
let i = 0;
i <
Math.max.apply(
null,
arrays.map((array) => array.length)
);
i++
) {
for (const array of arrays) {
if (i < array.length) {
result.push(array[i]);
}
}
}
return result;
}
/**
* Resolver implementation that handles DNS names and IP addresses.
*/
class DnsResolver implements Resolver {
private readonly ipResult: SubchannelAddress[] | null;
private readonly dnsHostname: string | null;
private readonly port: number | null;
private pendingLookupPromise: Promise<dns.LookupAddress[]> | null = null;
private pendingTxtPromise: Promise<string[][]> | null = null;
private latestLookupResult: TcpSubchannelAddress[] | null = null;
private latestServiceConfig: ServiceConfig | null = null;
private latestServiceConfigError: StatusObject | null = null;
private percentage: number;
private defaultResolutionError: StatusObject;
constructor(
private target: GrpcUri,
private listener: ResolverListener,
channelOptions: ChannelOptions
) {
trace('Resolver constructed for target ' + uriToString(target));
const hostPort = splitHostPort(target.path);
if (hostPort === null) {
this.ipResult = null;
this.dnsHostname = null;
this.port = null;
} else {
if (isIPv4(hostPort.host) || isIPv6(hostPort.host)) {
this.ipResult = [
{
host: hostPort.host,
port: hostPort.port ?? DEFAULT_PORT,
},
];
this.dnsHostname = null;
this.port = null;
} else {
this.ipResult = null;
this.dnsHostname = hostPort.host;
this.port = hostPort.port ?? DEFAULT_PORT;
}
}
this.percentage = Math.random() * 100;
this.defaultResolutionError = {
code: Status.UNAVAILABLE,
details: `Name resolution failed for target ${uriToString(this.target)}`,
metadata: new Metadata(),
};
}
/**
* If the target is an IP address, just provide that address as a result.
* Otherwise, initiate A, AAAA, and TXT lookups
*/
private startResolution() {
if (this.ipResult !== null) {
trace('Returning IP address for target ' + uriToString(this.target));
setImmediate(() => {
this.listener.onSuccessfulResolution(
this.ipResult!,
null,
null,
null,
{}
);
});
return;
}
if (this.dnsHostname === null) {
setImmediate(() => {
this.listener.onError({
code: Status.UNAVAILABLE,
details: `Failed to parse DNS address ${uriToString(this.target)}`,
metadata: new Metadata(),
});
});
} else {
/* We clear out latestLookupResult here to ensure that it contains the
* latest result since the last time we started resolving. That way, the
* TXT resolution handler can use it, but only if it finishes second. We
* don't clear out any previous service config results because it's
* better to use a service config that's slightly out of date than to
* revert to an effectively blank one. */
this.latestLookupResult = null;
const hostname: string = this.dnsHostname;
/* We lookup both address families here and then split them up later
* because when looking up a single family, dns.lookup outputs an error
* if the name exists but there are no records for that family, and that
* error is indistinguishable from other kinds of errors */
this.pendingLookupPromise = dnsLookupPromise(hostname, { all: true });
this.pendingLookupPromise.then(
(addressList) => {
this.pendingLookupPromise = null;
const ip4Addresses: dns.LookupAddress[] = addressList.filter(
(addr) => addr.family === 4
);
const ip6Addresses: dns.LookupAddress[] = addressList.filter(
(addr) => addr.family === 6
);
this.latestLookupResult = mergeArrays(
ip6Addresses,
ip4Addresses
).map((addr) => ({ host: addr.address, port: +this.port! }));
const allAddressesString: string =
'[' +
this.latestLookupResult
.map((addr) => addr.host + ':' + addr.port)
.join(',') +
']';
trace(
'Resolved addresses for target ' +
uriToString(this.target) +
': ' +
allAddressesString
);
if (this.latestLookupResult.length === 0) {
this.listener.onError(this.defaultResolutionError);
return;
}
/* If the TXT lookup has not yet finished, both of the last two
* arguments will be null, which is the equivalent of getting an
* empty TXT response. When the TXT lookup does finish, its handler
* can update the service config by using the same address list */
this.listener.onSuccessfulResolution(
this.latestLookupResult,
this.latestServiceConfig,
this.latestServiceConfigError,
null,
{}
);
},
(err) => {
trace(
'Resolution error for target ' +
uriToString(this.target) +
': ' +
(err as Error).message
);
this.pendingLookupPromise = null;
this.listener.onError(this.defaultResolutionError);
}
);
/* If there already is a still-pending TXT resolution, we can just use
* that result when it comes in */
if (this.pendingTxtPromise === null) {
/* We handle the TXT query promise differently than the others because
* the name resolution attempt as a whole is a success even if the TXT
* lookup fails */
this.pendingTxtPromise = resolveTxtPromise(hostname);
this.pendingTxtPromise.then(
(txtRecord) => {
this.pendingTxtPromise = null;
try {
this.latestServiceConfig = extractAndSelectServiceConfig(
txtRecord,
this.percentage
);
} catch (err) {
this.latestServiceConfigError = {
code: Status.UNAVAILABLE,
details: 'Parsing service config failed',
metadata: new Metadata(),
};
}
if (this.latestLookupResult !== null) {
/* We rely here on the assumption that calling this function with
* identical parameters will be essentialy idempotent, and calling
* it with the same address list and a different service config
* should result in a fast and seamless switchover. */
this.listener.onSuccessfulResolution(
this.latestLookupResult,
this.latestServiceConfig,
this.latestServiceConfigError,
null,
{}
);
}
},
(err) => {
/* If TXT lookup fails we should do nothing, which means that we
* continue to use the result of the most recent successful lookup,
* or the default null config object if there has never been a
* successful lookup. We do not set the latestServiceConfigError
* here because that is specifically used for response validation
* errors. We still need to handle this error so that it does not
* bubble up as an unhandled promise rejection. */
}
);
}
}
}
updateResolution() {
trace('Resolution update requested for target ' + uriToString(this.target));
if (this.pendingLookupPromise === null) {
this.startResolution();
}
}
destroy() {
/* Do nothing. There is not a practical way to cancel in-flight DNS
* requests, and after this function is called we can expect that
* updateResolution will not be called again. */
}
/**
* Get the default authority for the given target. For IP targets, that is
* the IP address. For DNS targets, it is the hostname.
* @param target
*/
static getDefaultAuthority(target: GrpcUri): string {
return target.path;
}
}
/**
* Set up the DNS resolver class by registering it as the handler for the
* "dns:" prefix and as the default resolver.
*/
export function setup(): void {
registerResolver('dns', DnsResolver);
registerDefaultScheme('dns');
}
export interface DnsUrl {
host: string;
port?: string;
}
+116
View File
@@ -0,0 +1,116 @@
/*
* Copyright 2021 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 { isIPv4, isIPv6 } from 'net';
import { StatusObject } from './call-stream';
import { ChannelOptions } from './channel-options';
import { LogVerbosity, Status } from './constants';
import { Metadata } from './metadata';
import { registerResolver, Resolver, ResolverListener } from './resolver';
import { SubchannelAddress } from './subchannel-address';
import { GrpcUri, splitHostPort, uriToString } from './uri-parser';
import * as logging from './logging';
const TRACER_NAME = 'ip_resolver';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
const IPV4_SCHEME = 'ipv4';
const IPV6_SCHEME = 'ipv6';
/**
* The default TCP port to connect to if not explicitly specified in the target.
*/
const DEFAULT_PORT = 443;
class IpResolver implements Resolver {
private addresses: SubchannelAddress[] = [];
private error: StatusObject | null = null;
constructor(
private target: GrpcUri,
private listener: ResolverListener,
channelOptions: ChannelOptions
) {
trace('Resolver constructed for target ' + uriToString(target));
const addresses: SubchannelAddress[] = [];
if (!(target.scheme === IPV4_SCHEME || target.scheme === IPV6_SCHEME)) {
this.error = {
code: Status.UNAVAILABLE,
details: `Unrecognized scheme ${target.scheme} in IP resolver`,
metadata: new Metadata(),
};
return;
}
const pathList = target.path.split(',');
for (const path of pathList) {
const hostPort = splitHostPort(path);
if (hostPort === null) {
this.error = {
code: Status.UNAVAILABLE,
details: `Failed to parse ${target.scheme} address ${path}`,
metadata: new Metadata(),
};
return;
}
if (
(target.scheme === IPV4_SCHEME && !isIPv4(hostPort.host)) ||
(target.scheme === IPV6_SCHEME && !isIPv6(hostPort.host))
) {
this.error = {
code: Status.UNAVAILABLE,
details: `Failed to parse ${target.scheme} address ${path}`,
metadata: new Metadata(),
};
return;
}
addresses.push({
host: hostPort.host,
port: hostPort.port ?? DEFAULT_PORT,
});
}
this.addresses = addresses;
trace('Parsed ' + target.scheme + ' address list ' + this.addresses);
}
updateResolution(): void {
process.nextTick(() => {
if (this.error) {
this.listener.onError(this.error);
} else {
this.listener.onSuccessfulResolution(
this.addresses,
null,
null,
null,
{}
);
}
});
}
destroy(): void {
// This resolver owns no resources, so we do nothing here.
}
static getDefaultAuthority(target: GrpcUri): string {
return target.path.split(',')[0];
}
}
export function setup() {
registerResolver(IPV4_SCHEME, IpResolver);
registerResolver(IPV6_SCHEME, IpResolver);
}
+59
View File
@@ -0,0 +1,59 @@
/*
* 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 { Resolver, ResolverListener, registerResolver } from './resolver';
import { SubchannelAddress } from './subchannel-address';
import { GrpcUri } from './uri-parser';
import { ChannelOptions } from './channel-options';
class UdsResolver implements Resolver {
private addresses: SubchannelAddress[] = [];
constructor(
target: GrpcUri,
private listener: ResolverListener,
channelOptions: ChannelOptions
) {
let path: string;
if (target.authority === '') {
path = '/' + target.path;
} else {
path = target.path;
}
this.addresses = [{ path }];
}
updateResolution(): void {
process.nextTick(
this.listener.onSuccessfulResolution,
this.addresses,
null,
null,
null,
{}
);
}
destroy() {
// This resolver owns no resources, so we do nothing here.
}
static getDefaultAuthority(target: GrpcUri): string {
return 'localhost';
}
}
export function setup() {
registerResolver('unix', UdsResolver);
}
+177
View File
@@ -0,0 +1,177 @@
/*
* 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 { MethodConfig, ServiceConfig } from './service-config';
import { StatusObject } from './call-stream';
import { SubchannelAddress } from './subchannel-address';
import { GrpcUri, uriToString } from './uri-parser';
import { ChannelOptions } from './channel-options';
import { Metadata } from './metadata';
import { Status } from './constants';
import { Filter, FilterFactory } from './filter';
export interface CallConfig {
methodConfig: MethodConfig;
onCommitted?: () => void;
pickInformation: { [key: string]: string };
status: Status;
dynamicFilterFactories: FilterFactory<Filter>[];
}
/**
* Selects a configuration for a method given the name and metadata. Defined in
* https://github.com/grpc/proposal/blob/master/A31-xds-timeout-support-and-config-selector.md#new-functionality-in-grpc
*/
export interface ConfigSelector {
(methodName: string, metadata: Metadata): CallConfig;
}
/**
* A listener object passed to the resolver's constructor that provides name
* resolution updates back to the resolver's owner.
*/
export interface ResolverListener {
/**
* Called whenever the resolver has new name resolution results to report
* @param addressList The new list of backend addresses
* @param serviceConfig The new service configuration corresponding to the
* `addressList`. Will be `null` if no service configuration was
* retrieved or if the service configuration was invalid
* @param serviceConfigError If non-`null`, indicates that the retrieved
* service configuration was invalid
*/
onSuccessfulResolution(
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: StatusObject | null,
configSelector: ConfigSelector | null,
attributes: { [key: string]: unknown }
): void;
/**
* Called whenever a name resolution attempt fails.
* @param error Describes how resolution failed
*/
onError(error: StatusObject): void;
}
/**
* A resolver class that handles one or more of the name syntax schemes defined
* in the [gRPC Name Resolution document](https://github.com/grpc/grpc/blob/master/doc/naming.md)
*/
export interface Resolver {
/**
* Indicates that the caller wants new name resolution data. Calling this
* function may eventually result in calling one of the `ResolverListener`
* functions, but that is not guaranteed. Those functions will never be
* called synchronously with the constructor or updateResolution.
*/
updateResolution(): void;
/**
* Destroy the resolver. Should be called when the owning channel shuts down.
*/
destroy(): void;
}
export interface ResolverConstructor {
new (
target: GrpcUri,
listener: ResolverListener,
channelOptions: ChannelOptions
): Resolver;
/**
* Get the default authority for a target. This loosely corresponds to that
* target's hostname. Throws an error if this resolver class cannot parse the
* `target`.
* @param target
*/
getDefaultAuthority(target: GrpcUri): string;
}
const registeredResolvers: { [scheme: string]: ResolverConstructor } = {};
let defaultScheme: string | null = null;
/**
* Register a resolver class to handle target names prefixed with the `prefix`
* string. This prefix should correspond to a URI scheme name listed in the
* [gRPC Name Resolution document](https://github.com/grpc/grpc/blob/master/doc/naming.md)
* @param prefix
* @param resolverClass
*/
export function registerResolver(
scheme: string,
resolverClass: ResolverConstructor
) {
registeredResolvers[scheme] = resolverClass;
}
/**
* Register a default resolver to handle target names that do not start with
* any registered prefix.
* @param resolverClass
*/
export function registerDefaultScheme(scheme: string) {
defaultScheme = scheme;
}
/**
* Create a name resolver for the specified target, if possible. Throws an
* error if no such name resolver can be created.
* @param target
* @param listener
*/
export function createResolver(
target: GrpcUri,
listener: ResolverListener,
options: ChannelOptions
): Resolver {
if (target.scheme !== undefined && target.scheme in registeredResolvers) {
return new registeredResolvers[target.scheme](target, listener, options);
} else {
throw new Error(
`No resolver could be created for target ${uriToString(target)}`
);
}
}
/**
* Get the default authority for the specified target, if possible. Throws an
* error if no registered name resolver can parse that target string.
* @param target
*/
export function getDefaultAuthority(target: GrpcUri): string {
if (target.scheme !== undefined && target.scheme in registeredResolvers) {
return registeredResolvers[target.scheme].getDefaultAuthority(target);
} else {
throw new Error(`Invalid target ${uriToString(target)}`);
}
}
export function mapUriDefaultScheme(target: GrpcUri): GrpcUri | null {
if (target.scheme === undefined || !(target.scheme in registeredResolvers)) {
if (defaultScheme !== null) {
return {
scheme: defaultScheme,
authority: undefined,
path: uriToString(target),
};
} else {
return null;
}
}
return target;
}
+333
View File
@@ -0,0 +1,333 @@
/*
* 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 {
ChannelControlHelper,
LoadBalancer,
LoadBalancingConfig,
getFirstUsableConfig,
} from './load-balancer';
import { ServiceConfig, validateServiceConfig } from './service-config';
import { ConnectivityState } from './connectivity-state';
import { ConfigSelector, createResolver, Resolver } from './resolver';
import { ServiceError } from './call';
import { Picker, UnavailablePicker, QueuePicker } from './picker';
import { BackoffOptions, BackoffTimeout } from './backoff-timeout';
import { Status } from './constants';
import { StatusObject } from './call-stream';
import { Metadata } from './metadata';
import * as logging from './logging';
import { LogVerbosity } from './constants';
import { SubchannelAddress } from './subchannel-address';
import { GrpcUri, uriToString } from './uri-parser';
import { ChildLoadBalancerHandler } from './load-balancer-child-handler';
import { ChannelOptions } from './channel-options';
import { PickFirstLoadBalancingConfig } from './load-balancer-pick-first';
const TRACER_NAME = 'resolving_load_balancer';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
const DEFAULT_LOAD_BALANCER_NAME = 'pick_first';
function getDefaultConfigSelector(
serviceConfig: ServiceConfig | null
): ConfigSelector {
return function defaultConfigSelector(
methodName: string,
metadata: Metadata
) {
const splitName = methodName.split('/').filter((x) => x.length > 0);
const service = splitName[0] ?? '';
const method = splitName[1] ?? '';
if (serviceConfig && serviceConfig.methodConfig) {
for (const methodConfig of serviceConfig.methodConfig) {
for (const name of methodConfig.name) {
if (
name.service === service &&
(name.method === undefined || name.method === method)
) {
return {
methodConfig: methodConfig,
pickInformation: {},
status: Status.OK,
dynamicFilterFactories: []
};
}
}
}
}
return {
methodConfig: { name: [] },
pickInformation: {},
status: Status.OK,
dynamicFilterFactories: []
};
};
}
export interface ResolutionCallback {
(configSelector: ConfigSelector): void;
}
export interface ResolutionFailureCallback {
(status: StatusObject): void;
}
export class ResolvingLoadBalancer implements LoadBalancer {
/**
* The resolver class constructed for the target address.
*/
private innerResolver: Resolver;
private childLoadBalancer: ChildLoadBalancerHandler;
private latestChildState: ConnectivityState = ConnectivityState.IDLE;
private latestChildPicker: Picker = new QueuePicker(this);
/**
* This resolving load balancer's current connectivity state.
*/
private currentState: ConnectivityState = ConnectivityState.IDLE;
private readonly defaultServiceConfig: ServiceConfig;
/**
* The service config object from the last successful resolution, if
* available. A value of null indicates that we have not yet received a valid
* service config from the resolver.
*/
private previousServiceConfig: ServiceConfig | null = null;
/**
* The backoff timer for handling name resolution failures.
*/
private readonly backoffTimeout: BackoffTimeout;
/**
* Indicates whether we should attempt to resolve again after the backoff
* timer runs out.
*/
private continueResolving = false;
/**
* Wrapper class that behaves like a `LoadBalancer` and also handles name
* resolution internally.
* @param target The address of the backend to connect to.
* @param channelControlHelper `ChannelControlHelper` instance provided by
* this load balancer's owner.
* @param defaultServiceConfig The default service configuration to be used
* if none is provided by the name resolver. A `null` value indicates
* that the default behavior should be the default unconfigured behavior.
* In practice, that means using the "pick first" load balancer
* implmentation
*/
constructor(
private readonly target: GrpcUri,
private readonly channelControlHelper: ChannelControlHelper,
private readonly channelOptions: ChannelOptions,
private readonly onSuccessfulResolution: ResolutionCallback,
private readonly onFailedResolution: ResolutionFailureCallback
) {
if (channelOptions['grpc.service_config']) {
this.defaultServiceConfig = validateServiceConfig(
JSON.parse(channelOptions['grpc.service_config']!)
);
} else {
this.defaultServiceConfig = {
loadBalancingConfig: [],
methodConfig: [],
};
}
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
this.childLoadBalancer = new ChildLoadBalancerHandler({
createSubchannel: channelControlHelper.createSubchannel.bind(
channelControlHelper
),
requestReresolution: () => {
/* If the backoffTimeout is running, we're still backing off from
* making resolve requests, so we shouldn't make another one here.
* In that case, the backoff timer callback will call
* updateResolution */
if (this.backoffTimeout.isRunning()) {
this.continueResolving = true;
} else {
this.updateResolution();
}
},
updateState: (newState: ConnectivityState, picker: Picker) => {
this.latestChildState = newState;
this.latestChildPicker = picker;
this.updateState(newState, picker);
},
addChannelzChild: channelControlHelper.addChannelzChild.bind(
channelControlHelper
),
removeChannelzChild: channelControlHelper.removeChannelzChild.bind(
channelControlHelper
)
});
this.innerResolver = createResolver(
target,
{
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: ServiceError | null,
configSelector: ConfigSelector | null,
attributes: { [key: string]: unknown }
) => {
let workingServiceConfig: ServiceConfig | null = null;
/* This first group of conditionals implements the algorithm described
* in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
* in the section called "Behavior on receiving a new gRPC Config".
*/
if (serviceConfig === null) {
// Step 4 and 5
if (serviceConfigError === null) {
// Step 5
this.previousServiceConfig = null;
workingServiceConfig = this.defaultServiceConfig;
} else {
// Step 4
if (this.previousServiceConfig === null) {
// Step 4.ii
this.handleResolutionFailure(serviceConfigError);
} else {
// Step 4.i
workingServiceConfig = this.previousServiceConfig;
}
}
} else {
// Step 3
workingServiceConfig = serviceConfig;
this.previousServiceConfig = serviceConfig;
}
const workingConfigList =
workingServiceConfig?.loadBalancingConfig ?? [];
const loadBalancingConfig = getFirstUsableConfig(
workingConfigList,
true
);
if (loadBalancingConfig === null) {
// There were load balancing configs but none are supported. This counts as a resolution failure
this.handleResolutionFailure({
code: Status.UNAVAILABLE,
details:
'All load balancer options in service config are not compatible',
metadata: new Metadata(),
});
return;
}
this.childLoadBalancer.updateAddressList(
addressList,
loadBalancingConfig,
attributes
);
const finalServiceConfig =
workingServiceConfig ?? this.defaultServiceConfig;
this.onSuccessfulResolution(
configSelector ?? getDefaultConfigSelector(finalServiceConfig)
);
},
onError: (error: StatusObject) => {
this.handleResolutionFailure(error);
},
},
channelOptions
);
const backoffOptions: BackoffOptions = {
initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
};
this.backoffTimeout = new BackoffTimeout(() => {
if (this.continueResolving) {
this.updateResolution();
this.continueResolving = false;
} else {
this.updateState(this.latestChildState, this.latestChildPicker);
}
}, backoffOptions);
this.backoffTimeout.unref();
}
private updateResolution() {
this.innerResolver.updateResolution();
if (this.currentState === ConnectivityState.IDLE) {
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
}
}
private updateState(connectivityState: ConnectivityState, picker: Picker) {
trace(
uriToString(this.target) +
' ' +
ConnectivityState[this.currentState] +
' -> ' +
ConnectivityState[connectivityState]
);
// Ensure that this.exitIdle() is called by the picker
if (connectivityState === ConnectivityState.IDLE) {
picker = new QueuePicker(this);
}
this.currentState = connectivityState;
this.channelControlHelper.updateState(connectivityState, picker);
}
private handleResolutionFailure(error: StatusObject) {
if (this.latestChildState === ConnectivityState.IDLE) {
this.updateState(
ConnectivityState.TRANSIENT_FAILURE,
new UnavailablePicker(error)
);
this.onFailedResolution(error);
}
this.backoffTimeout.runOnce();
}
exitIdle() {
this.childLoadBalancer.exitIdle();
if (this.currentState === ConnectivityState.IDLE) {
if (this.backoffTimeout.isRunning()) {
this.continueResolving = true;
} else {
this.updateResolution();
}
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
}
}
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig | null
) {
throw new Error('updateAddressList not supported on ResolvingLoadBalancer');
}
resetBackoff() {
this.backoffTimeout.reset();
this.childLoadBalancer.resetBackoff();
}
destroy() {
this.childLoadBalancer.destroy();
this.innerResolver.destroy();
this.updateState(ConnectivityState.SHUTDOWN, new UnavailablePicker());
}
getTypeName() {
return 'resolving_load_balancer';
}
}
+824
View File
@@ -0,0 +1,824 @@
/*
* 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 { EventEmitter } from 'events';
import * as http2 from 'http2';
import { Duplex, Readable, Writable } from 'stream';
import { Deadline, StatusObject } from './call-stream';
import {
Status,
DEFAULT_MAX_SEND_MESSAGE_LENGTH,
DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH,
LogVerbosity,
} from './constants';
import { Deserialize, Serialize } from './make-client';
import { Metadata } from './metadata';
import { StreamDecoder } from './stream-decoder';
import { ObjectReadable, ObjectWritable } from './object-stream';
import { ChannelOptions } from './channel-options';
import * as logging from './logging';
const TRACER_NAME = 'server_call';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
interface DeadlineUnitIndexSignature {
[name: string]: number;
}
const GRPC_ACCEPT_ENCODING_HEADER = 'grpc-accept-encoding';
const GRPC_ENCODING_HEADER = 'grpc-encoding';
const GRPC_MESSAGE_HEADER = 'grpc-message';
const GRPC_STATUS_HEADER = 'grpc-status';
const GRPC_TIMEOUT_HEADER = 'grpc-timeout';
const DEADLINE_REGEX = /(\d{1,8})\s*([HMSmun])/;
const deadlineUnitsToMs: DeadlineUnitIndexSignature = {
H: 3600000,
M: 60000,
S: 1000,
m: 1,
u: 0.001,
n: 0.000001,
};
const defaultResponseHeaders = {
// TODO(cjihrig): Remove these encoding headers from the default response
// once compression is integrated.
[GRPC_ACCEPT_ENCODING_HEADER]: 'identity',
[GRPC_ENCODING_HEADER]: 'identity',
[http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK,
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto',
};
const defaultResponseOptions = {
waitForTrailers: true,
} as http2.ServerStreamResponseOptions;
export type ServerStatusResponse = Partial<StatusObject>;
export type ServerErrorResponse = ServerStatusResponse & Error;
export type ServerSurfaceCall = {
cancelled: boolean;
readonly metadata: Metadata;
getPeer(): string;
sendMetadata(responseMetadata: Metadata): void;
getDeadline(): Deadline;
} & EventEmitter;
export type ServerUnaryCall<RequestType, ResponseType> = ServerSurfaceCall & {
request: RequestType;
};
export type ServerReadableStream<
RequestType,
ResponseType
> = ServerSurfaceCall & ObjectReadable<RequestType>;
export type ServerWritableStream<
RequestType,
ResponseType
> = ServerSurfaceCall &
ObjectWritable<ResponseType> & {
request: RequestType;
end: (metadata?: Metadata) => void;
};
export type ServerDuplexStream<RequestType, ResponseType> = ServerSurfaceCall &
ObjectReadable<RequestType> &
ObjectWritable<ResponseType> & { end: (metadata?: Metadata) => void };
export class ServerUnaryCallImpl<RequestType, ResponseType>
extends EventEmitter
implements ServerUnaryCall<RequestType, ResponseType> {
cancelled: boolean;
constructor(
private call: Http2ServerCallStream<RequestType, ResponseType>,
public metadata: Metadata,
public request: RequestType
) {
super();
this.cancelled = false;
this.call.setupSurfaceCall(this);
}
getPeer(): string {
return this.call.getPeer();
}
sendMetadata(responseMetadata: Metadata): void {
this.call.sendMetadata(responseMetadata);
}
getDeadline(): Deadline {
return this.call.getDeadline();
}
}
export class ServerReadableStreamImpl<RequestType, ResponseType>
extends Readable
implements ServerReadableStream<RequestType, ResponseType> {
cancelled: boolean;
constructor(
private call: Http2ServerCallStream<RequestType, ResponseType>,
public metadata: Metadata,
public deserialize: Deserialize<RequestType>
) {
super({ objectMode: true });
this.cancelled = false;
this.call.setupSurfaceCall(this);
this.call.setupReadable(this);
}
_read(size: number) {
if (!this.call.consumeUnpushedMessages(this)) {
return;
}
this.call.resume();
}
getPeer(): string {
return this.call.getPeer();
}
sendMetadata(responseMetadata: Metadata): void {
this.call.sendMetadata(responseMetadata);
}
getDeadline(): Deadline {
return this.call.getDeadline();
}
}
export class ServerWritableStreamImpl<RequestType, ResponseType>
extends Writable
implements ServerWritableStream<RequestType, ResponseType> {
cancelled: boolean;
private trailingMetadata: Metadata;
constructor(
private call: Http2ServerCallStream<RequestType, ResponseType>,
public metadata: Metadata,
public serialize: Serialize<ResponseType>,
public request: RequestType
) {
super({ objectMode: true });
this.cancelled = false;
this.trailingMetadata = new Metadata();
this.call.setupSurfaceCall(this);
this.on('error', (err) => {
this.call.sendError(err);
this.end();
});
}
getPeer(): string {
return this.call.getPeer();
}
sendMetadata(responseMetadata: Metadata): void {
this.call.sendMetadata(responseMetadata);
}
getDeadline(): Deadline {
return this.call.getDeadline();
}
_write(
chunk: ResponseType,
encoding: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (...args: any[]) => void
) {
try {
const response = this.call.serializeMessage(chunk);
if (!this.call.write(response)) {
this.call.once('drain', callback);
return;
}
} catch (err) {
err.code = Status.INTERNAL;
this.emit('error', err);
}
callback();
}
_final(callback: Function): void {
this.call.sendStatus({
code: Status.OK,
details: 'OK',
metadata: this.trailingMetadata,
});
callback(null);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
end(metadata?: any) {
if (metadata) {
this.trailingMetadata = metadata;
}
super.end();
}
}
export class ServerDuplexStreamImpl<RequestType, ResponseType>
extends Duplex
implements ServerDuplexStream<RequestType, ResponseType> {
cancelled: boolean;
private trailingMetadata: Metadata;
constructor(
private call: Http2ServerCallStream<RequestType, ResponseType>,
public metadata: Metadata,
public serialize: Serialize<ResponseType>,
public deserialize: Deserialize<RequestType>
) {
super({ objectMode: true });
this.cancelled = false;
this.trailingMetadata = new Metadata();
this.call.setupSurfaceCall(this);
this.call.setupReadable(this);
this.on('error', (err) => {
this.call.sendError(err);
this.end();
});
}
getPeer(): string {
return this.call.getPeer();
}
sendMetadata(responseMetadata: Metadata): void {
this.call.sendMetadata(responseMetadata);
}
getDeadline(): Deadline {
return this.call.getDeadline();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
end(metadata?: any) {
if (metadata) {
this.trailingMetadata = metadata;
}
super.end();
}
}
ServerDuplexStreamImpl.prototype._read =
ServerReadableStreamImpl.prototype._read;
ServerDuplexStreamImpl.prototype._write =
ServerWritableStreamImpl.prototype._write;
ServerDuplexStreamImpl.prototype._final =
ServerWritableStreamImpl.prototype._final;
ServerDuplexStreamImpl.prototype.end = ServerWritableStreamImpl.prototype.end;
// Unary response callback signature.
export type sendUnaryData<ResponseType> = (
error: ServerErrorResponse | ServerStatusResponse | null,
value?: ResponseType | null,
trailer?: Metadata,
flags?: number
) => void;
// User provided handler for unary calls.
export type handleUnaryCall<RequestType, ResponseType> = (
call: ServerUnaryCall<RequestType, ResponseType>,
callback: sendUnaryData<ResponseType>
) => void;
// User provided handler for client streaming calls.
export type handleClientStreamingCall<RequestType, ResponseType> = (
call: ServerReadableStream<RequestType, ResponseType>,
callback: sendUnaryData<ResponseType>
) => void;
// User provided handler for server streaming calls.
export type handleServerStreamingCall<RequestType, ResponseType> = (
call: ServerWritableStream<RequestType, ResponseType>
) => void;
// User provided handler for bidirectional streaming calls.
export type handleBidiStreamingCall<RequestType, ResponseType> = (
call: ServerDuplexStream<RequestType, ResponseType>
) => void;
export type HandleCall<RequestType, ResponseType> =
| handleUnaryCall<RequestType, ResponseType>
| handleClientStreamingCall<RequestType, ResponseType>
| handleServerStreamingCall<RequestType, ResponseType>
| handleBidiStreamingCall<RequestType, ResponseType>;
export interface UnaryHandler<RequestType, ResponseType> {
func: handleUnaryCall<RequestType, ResponseType>;
serialize: Serialize<ResponseType>;
deserialize: Deserialize<RequestType>;
type: HandlerType;
path: string;
}
export interface ClientStreamingHandler<RequestType, ResponseType> {
func: handleClientStreamingCall<RequestType, ResponseType>;
serialize: Serialize<ResponseType>;
deserialize: Deserialize<RequestType>;
type: HandlerType;
path: string;
}
export interface ServerStreamingHandler<RequestType, ResponseType> {
func: handleServerStreamingCall<RequestType, ResponseType>;
serialize: Serialize<ResponseType>;
deserialize: Deserialize<RequestType>;
type: HandlerType;
path: string;
}
export interface BidiStreamingHandler<RequestType, ResponseType> {
func: handleBidiStreamingCall<RequestType, ResponseType>;
serialize: Serialize<ResponseType>;
deserialize: Deserialize<RequestType>;
type: HandlerType;
path: string;
}
export type Handler<RequestType, ResponseType> =
| UnaryHandler<RequestType, ResponseType>
| ClientStreamingHandler<RequestType, ResponseType>
| ServerStreamingHandler<RequestType, ResponseType>
| BidiStreamingHandler<RequestType, ResponseType>;
export type HandlerType = 'bidi' | 'clientStream' | 'serverStream' | 'unary';
// Internal class that wraps the HTTP2 request.
export class Http2ServerCallStream<
RequestType,
ResponseType
> extends EventEmitter {
cancelled = false;
deadlineTimer: NodeJS.Timer = setTimeout(() => {}, 0);
private deadline: Deadline = Infinity;
private wantTrailers = false;
private metadataSent = false;
private canPush = false;
private isPushPending = false;
private bufferedMessages: Array<Buffer | null> = [];
private messagesToPush: Array<RequestType | null> = [];
private maxSendMessageSize: number = DEFAULT_MAX_SEND_MESSAGE_LENGTH;
private maxReceiveMessageSize: number = DEFAULT_MAX_RECEIVE_MESSAGE_LENGTH;
constructor(
private stream: http2.ServerHttp2Stream,
private handler: Handler<RequestType, ResponseType>,
private options: ChannelOptions
) {
super();
this.stream.once('error', (err: ServerErrorResponse) => {
/* We need an error handler to avoid uncaught error event exceptions, but
* there is nothing we can reasonably do here. Any error event should
* have a corresponding close event, which handles emitting the cancelled
* event. And the stream is now in a bad state, so we can't reasonably
* expect to be able to send an error over it. */
});
this.stream.once('close', () => {
trace(
'Request to method ' +
this.handler?.path +
' stream closed with rstCode ' +
this.stream.rstCode
);
this.cancelled = true;
this.emit('cancelled', 'cancelled');
this.emit('streamEnd', false);
this.sendStatus({code: Status.CANCELLED, details: 'Cancelled by client', metadata: new Metadata()});
});
this.stream.on('drain', () => {
this.emit('drain');
});
if ('grpc.max_send_message_length' in options) {
this.maxSendMessageSize = options['grpc.max_send_message_length']!;
}
if ('grpc.max_receive_message_length' in options) {
this.maxReceiveMessageSize = options['grpc.max_receive_message_length']!;
}
// Clear noop timer
clearTimeout(this.deadlineTimer);
}
private checkCancelled(): boolean {
/* In some cases the stream can become destroyed before the close event
* fires. That creates a race condition that this check works around */
if (this.stream.destroyed || this.stream.closed) {
this.cancelled = true;
}
return this.cancelled;
}
sendMetadata(customMetadata?: Metadata) {
if (this.checkCancelled()) {
return;
}
if (this.metadataSent) {
return;
}
this.metadataSent = true;
const custom = customMetadata ? customMetadata.toHttp2Headers() : null;
// TODO(cjihrig): Include compression headers.
const headers = Object.assign({}, defaultResponseHeaders, custom);
this.stream.respond(headers, defaultResponseOptions);
}
receiveMetadata(headers: http2.IncomingHttpHeaders) {
const metadata = Metadata.fromHttp2Headers(headers);
// TODO(cjihrig): Receive compression metadata.
const timeoutHeader = metadata.get(GRPC_TIMEOUT_HEADER);
if (timeoutHeader.length > 0) {
const match = timeoutHeader[0].toString().match(DEADLINE_REGEX);
if (match === null) {
const err = new Error('Invalid deadline') as ServerErrorResponse;
err.code = Status.OUT_OF_RANGE;
this.sendError(err);
return;
}
const timeout = (+match[1] * deadlineUnitsToMs[match[2]]) | 0;
const now = new Date();
this.deadline = now.setMilliseconds(now.getMilliseconds() + timeout);
this.deadlineTimer = setTimeout(handleExpiredDeadline, timeout, this);
metadata.remove(GRPC_TIMEOUT_HEADER);
}
// Remove several headers that should not be propagated to the application
metadata.remove(http2.constants.HTTP2_HEADER_ACCEPT_ENCODING);
metadata.remove(http2.constants.HTTP2_HEADER_TE);
metadata.remove(http2.constants.HTTP2_HEADER_CONTENT_TYPE);
metadata.remove('grpc-encoding');
metadata.remove('grpc-accept-encoding');
return metadata;
}
receiveUnaryMessage(): Promise<RequestType> {
return new Promise((resolve, reject) => {
const stream = this.stream;
const chunks: Buffer[] = [];
let totalLength = 0;
stream.on('data', (data: Buffer) => {
chunks.push(data);
totalLength += data.byteLength;
});
stream.once('end', async () => {
try {
const requestBytes = Buffer.concat(chunks, totalLength);
if (
this.maxReceiveMessageSize !== -1 &&
requestBytes.length > this.maxReceiveMessageSize
) {
this.sendError({
code: Status.RESOURCE_EXHAUSTED,
details: `Received message larger than max (${requestBytes.length} vs. ${this.maxReceiveMessageSize})`,
});
resolve();
}
this.emit('receiveMessage');
resolve(this.deserializeMessage(requestBytes));
} catch (err) {
err.code = Status.INTERNAL;
this.sendError(err);
resolve();
}
});
});
}
serializeMessage(value: ResponseType) {
const messageBuffer = this.handler.serialize(value);
// TODO(cjihrig): Call compression aware serializeMessage().
const byteLength = messageBuffer.byteLength;
const output = Buffer.allocUnsafe(byteLength + 5);
output.writeUInt8(0, 0);
output.writeUInt32BE(byteLength, 1);
messageBuffer.copy(output, 5);
return output;
}
deserializeMessage(bytes: Buffer) {
// TODO(cjihrig): Call compression aware deserializeMessage().
const receivedMessage = bytes.slice(5);
return this.handler.deserialize(receivedMessage);
}
async sendUnaryMessage(
err: ServerErrorResponse | ServerStatusResponse | null,
value?: ResponseType | null,
metadata?: Metadata,
flags?: number
) {
if (this.checkCancelled()) {
return;
}
if (!metadata) {
metadata = new Metadata();
}
if (err) {
if (!Object.prototype.hasOwnProperty.call(err, 'metadata')) {
err.metadata = metadata;
}
this.sendError(err);
return;
}
try {
const response = this.serializeMessage(value!);
this.write(response);
this.sendStatus({ code: Status.OK, details: 'OK', metadata });
} catch (err) {
err.code = Status.INTERNAL;
this.sendError(err);
}
}
sendStatus(statusObj: StatusObject) {
this.emit('callEnd', statusObj.code);
this.emit('streamEnd', statusObj.code === Status.OK);
if (this.checkCancelled()) {
return;
}
trace(
'Request to method ' +
this.handler?.path +
' ended with status code: ' +
Status[statusObj.code] +
' details: ' +
statusObj.details
);
clearTimeout(this.deadlineTimer);
if (!this.wantTrailers) {
this.wantTrailers = true;
this.stream.once('wantTrailers', () => {
const trailersToSend = Object.assign(
{
[GRPC_STATUS_HEADER]: statusObj.code,
[GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details as string),
},
statusObj.metadata.toHttp2Headers()
);
this.stream.sendTrailers(trailersToSend);
});
this.sendMetadata();
this.stream.end();
}
}
sendError(error: ServerErrorResponse | ServerStatusResponse) {
const status: StatusObject = {
code: Status.UNKNOWN,
details: 'message' in error ? error.message : 'Unknown Error',
metadata:
'metadata' in error && error.metadata !== undefined
? error.metadata
: new Metadata(),
};
if (
'code' in error &&
typeof error.code === 'number' &&
Number.isInteger(error.code)
) {
status.code = error.code;
if ('details' in error && typeof error.details === 'string') {
status.details = error.details!;
}
}
this.sendStatus(status);
}
write(chunk: Buffer) {
if (this.checkCancelled()) {
return;
}
if (
this.maxSendMessageSize !== -1 &&
chunk.length > this.maxSendMessageSize
) {
this.sendError({
code: Status.RESOURCE_EXHAUSTED,
details: `Sent message larger than max (${chunk.length} vs. ${this.maxSendMessageSize})`,
});
return;
}
this.sendMetadata();
this.emit('sendMessage');
return this.stream.write(chunk);
}
resume() {
this.stream.resume();
}
setupSurfaceCall(call: ServerSurfaceCall) {
this.once('cancelled', (reason) => {
call.cancelled = true;
call.emit('cancelled', reason);
});
}
setupReadable(
readable:
| ServerReadableStream<RequestType, ResponseType>
| ServerDuplexStream<RequestType, ResponseType>
) {
const decoder = new StreamDecoder();
this.stream.on('data', async (data: Buffer) => {
const messages = decoder.write(data);
for (const message of messages) {
if (
this.maxReceiveMessageSize !== -1 &&
message.length > this.maxReceiveMessageSize
) {
this.sendError({
code: Status.RESOURCE_EXHAUSTED,
details: `Received message larger than max (${message.length} vs. ${this.maxReceiveMessageSize})`,
});
return;
}
this.emit('receiveMessage');
this.pushOrBufferMessage(readable, message);
}
});
this.stream.once('end', () => {
this.pushOrBufferMessage(readable, null);
});
}
consumeUnpushedMessages(
readable:
| ServerReadableStream<RequestType, ResponseType>
| ServerDuplexStream<RequestType, ResponseType>
): boolean {
this.canPush = true;
while (this.messagesToPush.length > 0) {
const nextMessage = this.messagesToPush.shift();
const canPush = readable.push(nextMessage);
if (nextMessage === null || canPush === false) {
this.canPush = false;
break;
}
}
return this.canPush;
}
private pushOrBufferMessage(
readable:
| ServerReadableStream<RequestType, ResponseType>
| ServerDuplexStream<RequestType, ResponseType>,
messageBytes: Buffer | null
): void {
if (this.isPushPending) {
this.bufferedMessages.push(messageBytes);
} else {
this.pushMessage(readable, messageBytes);
}
}
private async pushMessage(
readable:
| ServerReadableStream<RequestType, ResponseType>
| ServerDuplexStream<RequestType, ResponseType>,
messageBytes: Buffer | null
) {
if (messageBytes === null) {
if (this.canPush) {
readable.push(null);
} else {
this.messagesToPush.push(null);
}
return;
}
this.isPushPending = true;
try {
const deserialized = await this.deserializeMessage(messageBytes);
if (this.canPush) {
if (!readable.push(deserialized)) {
this.canPush = false;
this.stream.pause();
}
} else {
this.messagesToPush.push(deserialized);
}
} catch (error) {
// Ignore any remaining messages when errors occur.
this.bufferedMessages.length = 0;
if (
!(
'code' in error &&
typeof error.code === 'number' &&
Number.isInteger(error.code) &&
error.code >= Status.OK &&
error.code <= Status.UNAUTHENTICATED
)
) {
// The error code is not a valid gRPC code so its being overwritten.
error.code = Status.INTERNAL;
}
readable.emit('error', error);
}
this.isPushPending = false;
if (this.bufferedMessages.length > 0) {
this.pushMessage(
readable,
this.bufferedMessages.shift() as Buffer | null
);
}
}
getPeer(): string {
const socket = this.stream.session.socket;
if (socket.remoteAddress) {
if (socket.remotePort) {
return `${socket.remoteAddress}:${socket.remotePort}`;
} else {
return socket.remoteAddress;
}
} else {
return 'unknown';
}
}
getDeadline(): Deadline {
return this.deadline;
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
type UntypedServerCall = Http2ServerCallStream<any, any>;
function handleExpiredDeadline(call: UntypedServerCall) {
const err = new Error('Deadline exceeded') as ServerErrorResponse;
err.code = Status.DEADLINE_EXCEEDED;
call.sendError(err);
call.cancelled = true;
call.emit('cancelled', 'deadline');
}
+108
View File
@@ -0,0 +1,108 @@
/*
* 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 { SecureServerOptions } from 'http2';
import { CIPHER_SUITES, getDefaultRootsData } from './tls-helpers';
export interface KeyCertPair {
private_key: Buffer;
cert_chain: Buffer;
}
export abstract class ServerCredentials {
abstract _isSecure(): boolean;
abstract _getSettings(): SecureServerOptions | null;
static createInsecure(): ServerCredentials {
return new InsecureServerCredentials();
}
static createSsl(
rootCerts: Buffer | null,
keyCertPairs: KeyCertPair[],
checkClientCertificate = false
): ServerCredentials {
if (rootCerts !== null && !Buffer.isBuffer(rootCerts)) {
throw new TypeError('rootCerts must be null or a Buffer');
}
if (!Array.isArray(keyCertPairs)) {
throw new TypeError('keyCertPairs must be an array');
}
if (typeof checkClientCertificate !== 'boolean') {
throw new TypeError('checkClientCertificate must be a boolean');
}
const cert = [];
const key = [];
for (let i = 0; i < keyCertPairs.length; i++) {
const pair = keyCertPairs[i];
if (pair === null || typeof pair !== 'object') {
throw new TypeError(`keyCertPair[${i}] must be an object`);
}
if (!Buffer.isBuffer(pair.private_key)) {
throw new TypeError(`keyCertPair[${i}].private_key must be a Buffer`);
}
if (!Buffer.isBuffer(pair.cert_chain)) {
throw new TypeError(`keyCertPair[${i}].cert_chain must be a Buffer`);
}
cert.push(pair.cert_chain);
key.push(pair.private_key);
}
return new SecureServerCredentials({
ca: rootCerts || getDefaultRootsData() || undefined,
cert,
key,
requestCert: checkClientCertificate,
ciphers: CIPHER_SUITES,
});
}
}
class InsecureServerCredentials extends ServerCredentials {
_isSecure(): boolean {
return false;
}
_getSettings(): null {
return null;
}
}
class SecureServerCredentials extends ServerCredentials {
private options: SecureServerOptions;
constructor(options: SecureServerOptions) {
super();
this.options = options;
}
_isSecure(): boolean {
return true;
}
_getSettings(): SecureServerOptions {
return this.options;
}
}
+976
View File
@@ -0,0 +1,976 @@
/*
* 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 * as http2 from 'http2';
import { AddressInfo } from 'net';
import { ServiceError } from './call';
import { Status, LogVerbosity } from './constants';
import { Deserialize, Serialize, ServiceDefinition } from './make-client';
import { Metadata } from './metadata';
import {
BidiStreamingHandler,
ClientStreamingHandler,
HandleCall,
Handler,
HandlerType,
Http2ServerCallStream,
sendUnaryData,
ServerDuplexStream,
ServerDuplexStreamImpl,
ServerReadableStream,
ServerReadableStreamImpl,
ServerStreamingHandler,
ServerUnaryCall,
ServerUnaryCallImpl,
ServerWritableStream,
ServerWritableStreamImpl,
UnaryHandler,
ServerErrorResponse,
ServerStatusResponse,
} from './server-call';
import { ServerCredentials } from './server-credentials';
import { ChannelOptions } from './channel-options';
import {
createResolver,
ResolverListener,
mapUriDefaultScheme,
} from './resolver';
import * as logging from './logging';
import {
SubchannelAddress,
TcpSubchannelAddress,
isTcpSubchannelAddress,
subchannelAddressToString,
stringToSubchannelAddress,
} from './subchannel-address';
import { parseUri } from './uri-parser';
import { ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzServer, registerChannelzSocket, ServerInfo, ServerRef, SocketInfo, SocketRef, TlsInfo, unregisterChannelzRef } from './channelz';
import { CipherNameAndProtocol, TLSSocket } from 'tls';
const TRACER_NAME = 'server';
interface BindResult {
port: number;
count: number;
}
function noop(): void {}
function getUnimplementedStatusResponse(
methodName: string
): Partial<ServiceError> {
return {
code: Status.UNIMPLEMENTED,
details: `The server does not implement the method ${methodName}`,
metadata: new Metadata(),
};
}
/* eslint-disable @typescript-eslint/no-explicit-any */
type UntypedUnaryHandler = UnaryHandler<any, any>;
type UntypedClientStreamingHandler = ClientStreamingHandler<any, any>;
type UntypedServerStreamingHandler = ServerStreamingHandler<any, any>;
type UntypedBidiStreamingHandler = BidiStreamingHandler<any, any>;
export type UntypedHandleCall = HandleCall<any, any>;
type UntypedHandler = Handler<any, any>;
export interface UntypedServiceImplementation {
[name: string]: UntypedHandleCall;
}
function getDefaultHandler(handlerType: HandlerType, methodName: string) {
const unimplementedStatusResponse = getUnimplementedStatusResponse(
methodName
);
switch (handlerType) {
case 'unary':
return (
call: ServerUnaryCall<any, any>,
callback: sendUnaryData<any>
) => {
callback(unimplementedStatusResponse as ServiceError, null);
};
case 'clientStream':
return (
call: ServerReadableStream<any, any>,
callback: sendUnaryData<any>
) => {
callback(unimplementedStatusResponse as ServiceError, null);
};
case 'serverStream':
return (call: ServerWritableStream<any, any>) => {
call.emit('error', unimplementedStatusResponse);
};
case 'bidi':
return (call: ServerDuplexStream<any, any>) => {
call.emit('error', unimplementedStatusResponse);
};
default:
throw new Error(`Invalid handlerType ${handlerType}`);
}
}
interface ChannelzSessionInfo {
ref: SocketRef;
streamTracker: ChannelzCallTracker;
messagesSent: number;
messagesReceived: number;
lastMessageSentTimestamp: Date | null;
lastMessageReceivedTimestamp: Date | null;
}
interface ChannelzListenerInfo {
ref: SocketRef;
}
export class Server {
private http2ServerList: { server: (http2.Http2Server | http2.Http2SecureServer), channelzRef: SocketRef }[] = [];
private handlers: Map<string, UntypedHandler> = new Map<
string,
UntypedHandler
>();
private sessions = new Map<http2.ServerHttp2Session, ChannelzSessionInfo>();
private started = false;
private options: ChannelOptions;
// Channelz Info
private readonly channelzEnabled: boolean = true;
private channelzRef: ServerRef;
private channelzTrace = new ChannelzTrace();
private callTracker = new ChannelzCallTracker();
private listenerChildrenTracker = new ChannelzChildrenTracker();
private sessionChildrenTracker = new ChannelzChildrenTracker();
constructor(options?: ChannelOptions) {
this.options = options ?? {};
if (this.options['grpc.enable_channelz'] === 0) {
this.channelzEnabled = false;
}
if (this.channelzEnabled) {
this.channelzRef = registerChannelzServer(() => this.getChannelzInfo());
this.channelzTrace.addTrace('CT_INFO', 'Server created');
this.trace('Server constructed');
} else {
// Dummy channelz ref that will never be used
this.channelzRef = {
kind: 'server',
id: -1
};
}
}
private getChannelzInfo(): ServerInfo {
return {
trace: this.channelzTrace,
callTracker: this.callTracker,
listenerChildren: this.listenerChildrenTracker.getChildLists(),
sessionChildren: this.sessionChildrenTracker.getChildLists()
};
}
private getChannelzSessionInfoGetter(session: http2.ServerHttp2Session): () => SocketInfo {
return () => {
const sessionInfo = this.sessions.get(session)!;
const sessionSocket = session.socket;
const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null;
const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress!, sessionSocket.localPort) : null;
let tlsInfo: TlsInfo | null;
if (session.encrypted) {
const tlsSocket: TLSSocket = sessionSocket as TLSSocket;
const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher();
const certificate = tlsSocket.getCertificate();
const peerCertificate = tlsSocket.getPeerCertificate();
tlsInfo = {
cipherSuiteStandardName: cipherInfo.standardName ?? null,
cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name,
localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null,
remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null
};
} else {
tlsInfo = null;
}
const socketInfo: SocketInfo = {
remoteAddress: remoteAddress,
localAddress: localAddress,
security: tlsInfo,
remoteName: null,
streamsStarted: sessionInfo.streamTracker.callsStarted,
streamsSucceeded: sessionInfo.streamTracker.callsSucceeded,
streamsFailed: sessionInfo.streamTracker.callsFailed,
messagesSent: sessionInfo.messagesSent,
messagesReceived: sessionInfo.messagesReceived,
keepAlivesSent: 0,
lastLocalStreamCreatedTimestamp: null,
lastRemoteStreamCreatedTimestamp: sessionInfo.streamTracker.lastCallStartedTimestamp,
lastMessageSentTimestamp: sessionInfo.lastMessageSentTimestamp,
lastMessageReceivedTimestamp: sessionInfo.lastMessageReceivedTimestamp,
localFlowControlWindow: session.state.localWindowSize ?? null,
remoteFlowControlWindow: session.state.remoteWindowSize ?? null
};
return socketInfo;
};
}
private trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, '(' + this.channelzRef.id + ') ' + text);
}
addProtoService(): void {
throw new Error('Not implemented. Use addService() instead');
}
addService(
service: ServiceDefinition,
implementation: UntypedServiceImplementation
): void {
if (
service === null ||
typeof service !== 'object' ||
implementation === null ||
typeof implementation !== 'object'
) {
throw new Error('addService() requires two objects as arguments');
}
const serviceKeys = Object.keys(service);
if (serviceKeys.length === 0) {
throw new Error('Cannot add an empty service to a server');
}
serviceKeys.forEach((name) => {
const attrs = service[name];
let methodType: HandlerType;
if (attrs.requestStream) {
if (attrs.responseStream) {
methodType = 'bidi';
} else {
methodType = 'clientStream';
}
} else {
if (attrs.responseStream) {
methodType = 'serverStream';
} else {
methodType = 'unary';
}
}
let implFn = implementation[name];
let impl;
if (implFn === undefined && typeof attrs.originalName === 'string') {
implFn = implementation[attrs.originalName];
}
if (implFn !== undefined) {
impl = implFn.bind(implementation);
} else {
impl = getDefaultHandler(methodType, name);
}
const success = this.register(
attrs.path,
impl as UntypedHandleCall,
attrs.responseSerialize,
attrs.requestDeserialize,
methodType
);
if (success === false) {
throw new Error(`Method handler for ${attrs.path} already provided.`);
}
});
}
removeService(service: ServiceDefinition): void {
if (service === null || typeof service !== 'object') {
throw new Error('removeService() requires object as argument');
}
const serviceKeys = Object.keys(service);
serviceKeys.forEach((name) => {
const attrs = service[name];
this.unregister(attrs.path);
});
}
bind(port: string, creds: ServerCredentials): void {
throw new Error('Not implemented. Use bindAsync() instead');
}
bindAsync(
port: string,
creds: ServerCredentials,
callback: (error: Error | null, port: number) => void
): void {
if (this.started === true) {
throw new Error('server is already started');
}
if (typeof port !== 'string') {
throw new TypeError('port must be a string');
}
if (creds === null || !(creds instanceof ServerCredentials)) {
throw new TypeError('creds must be a ServerCredentials object');
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const initialPortUri = parseUri(port);
if (initialPortUri === null) {
throw new Error(`Could not parse port "${port}"`);
}
const portUri = mapUriDefaultScheme(initialPortUri);
if (portUri === null) {
throw new Error(`Could not get a default scheme for port "${port}"`);
}
const serverOptions: http2.ServerOptions = {
maxSendHeaderBlockLength: Number.MAX_SAFE_INTEGER,
};
if ('grpc-node.max_session_memory' in this.options) {
serverOptions.maxSessionMemory = this.options[
'grpc-node.max_session_memory'
];
}
if ('grpc.max_concurrent_streams' in this.options) {
serverOptions.settings = {
maxConcurrentStreams: this.options['grpc.max_concurrent_streams'],
};
}
const deferredCallback = (error: Error | null, port: number) => {
process.nextTick(() => callback(error, port));
}
const setupServer = (): http2.Http2Server | http2.Http2SecureServer => {
let http2Server: http2.Http2Server | http2.Http2SecureServer;
if (creds._isSecure()) {
const secureServerOptions = Object.assign(
serverOptions,
creds._getSettings()!
);
http2Server = http2.createSecureServer(secureServerOptions);
} else {
http2Server = http2.createServer(serverOptions);
}
http2Server.setTimeout(0, noop);
this._setupHandlers(http2Server);
return http2Server;
};
const bindSpecificPort = (
addressList: SubchannelAddress[],
portNum: number,
previousCount: number
): Promise<BindResult> => {
if (addressList.length === 0) {
return Promise.resolve({ port: portNum, count: previousCount });
}
return Promise.all(
addressList.map((address) => {
this.trace('Attempting to bind ' + subchannelAddressToString(address));
let addr: SubchannelAddress;
if (isTcpSubchannelAddress(address)) {
addr = {
host: (address as TcpSubchannelAddress).host,
port: portNum,
};
} else {
addr = address;
}
const http2Server = setupServer();
return new Promise<number | Error>((resolve, reject) => {
const onError = (err: Error) => {
this.trace('Failed to bind ' + subchannelAddressToString(address) + ' with error ' + err.message);
resolve(err);
}
http2Server.once('error', onError);
http2Server.listen(addr, () => {
const boundAddress = http2Server.address()!;
let boundSubchannelAddress: SubchannelAddress;
if (typeof boundAddress === 'string') {
boundSubchannelAddress = {
path: boundAddress
};
} else {
boundSubchannelAddress = {
host: boundAddress.address,
port: boundAddress.port
}
}
const channelzRef = registerChannelzSocket(subchannelAddressToString(boundSubchannelAddress), () => {
return {
localAddress: boundSubchannelAddress,
remoteAddress: null,
security: null,
remoteName: null,
streamsStarted: 0,
streamsSucceeded: 0,
streamsFailed: 0,
messagesSent: 0,
messagesReceived: 0,
keepAlivesSent: 0,
lastLocalStreamCreatedTimestamp: null,
lastRemoteStreamCreatedTimestamp: null,
lastMessageSentTimestamp: null,
lastMessageReceivedTimestamp: null,
localFlowControlWindow: null,
remoteFlowControlWindow: null
};
});
this.listenerChildrenTracker.refChild(channelzRef);
this.http2ServerList.push({server: http2Server, channelzRef: channelzRef});
this.trace('Successfully bound ' + subchannelAddressToString(boundSubchannelAddress));
resolve('port' in boundSubchannelAddress ? boundSubchannelAddress.port : portNum);
http2Server.removeListener('error', onError);
});
});
})
).then((results) => {
let count = 0;
for (const result of results) {
if (typeof result === 'number') {
count += 1;
if (result !== portNum) {
throw new Error(
'Invalid state: multiple port numbers added from single address'
);
}
}
}
return {
port: portNum,
count: count + previousCount,
};
});
};
const bindWildcardPort = (
addressList: SubchannelAddress[]
): Promise<BindResult> => {
if (addressList.length === 0) {
return Promise.resolve<BindResult>({ port: 0, count: 0 });
}
const address = addressList[0];
const http2Server = setupServer();
return new Promise<BindResult>((resolve, reject) => {
const onError = (err: Error) => {
this.trace('Failed to bind ' + subchannelAddressToString(address) + ' with error ' + err.message);
resolve(bindWildcardPort(addressList.slice(1)));
}
http2Server.once('error', onError);
http2Server.listen(address, () => {
const boundAddress = http2Server.address() as AddressInfo;
const boundSubchannelAddress: SubchannelAddress = {
host: boundAddress.address,
port: boundAddress.port
};
const channelzRef = registerChannelzSocket(subchannelAddressToString(boundSubchannelAddress), () => {
return {
localAddress: boundSubchannelAddress,
remoteAddress: null,
security: null,
remoteName: null,
streamsStarted: 0,
streamsSucceeded: 0,
streamsFailed: 0,
messagesSent: 0,
messagesReceived: 0,
keepAlivesSent: 0,
lastLocalStreamCreatedTimestamp: null,
lastRemoteStreamCreatedTimestamp: null,
lastMessageSentTimestamp: null,
lastMessageReceivedTimestamp: null,
localFlowControlWindow: null,
remoteFlowControlWindow: null
};
});
this.listenerChildrenTracker.refChild(channelzRef);
this.http2ServerList.push({server: http2Server, channelzRef: channelzRef});
this.trace('Successfully bound ' + subchannelAddressToString(boundSubchannelAddress));
resolve(
bindSpecificPort(
addressList.slice(1),
boundAddress.port,
1
)
);
http2Server.removeListener('error', onError);
});
});
};
const resolverListener: ResolverListener = {
onSuccessfulResolution: (
addressList,
serviceConfig,
serviceConfigError
) => {
// We only want one resolution result. Discard all future results
resolverListener.onSuccessfulResolution = () => {};
if (addressList.length === 0) {
deferredCallback(new Error(`No addresses resolved for port ${port}`), 0);
return;
}
let bindResultPromise: Promise<BindResult>;
if (isTcpSubchannelAddress(addressList[0])) {
if (addressList[0].port === 0) {
bindResultPromise = bindWildcardPort(addressList);
} else {
bindResultPromise = bindSpecificPort(
addressList,
addressList[0].port,
0
);
}
} else {
// Use an arbitrary non-zero port for non-TCP addresses
bindResultPromise = bindSpecificPort(addressList, 1, 0);
}
bindResultPromise.then(
(bindResult) => {
if (bindResult.count === 0) {
const errorString = `No address added out of total ${addressList.length} resolved`;
logging.log(LogVerbosity.ERROR, errorString);
deferredCallback(new Error(errorString), 0);
} else {
if (bindResult.count < addressList.length) {
logging.log(
LogVerbosity.INFO,
`WARNING Only ${bindResult.count} addresses added out of total ${addressList.length} resolved`
);
}
deferredCallback(null, bindResult.port);
}
},
(error) => {
const errorString = `No address added out of total ${addressList.length} resolved`;
logging.log(LogVerbosity.ERROR, errorString);
deferredCallback(new Error(errorString), 0);
}
);
},
onError: (error) => {
deferredCallback(new Error(error.details), 0);
},
};
const resolver = createResolver(portUri, resolverListener, this.options);
resolver.updateResolution();
}
forceShutdown(): void {
// Close the server if it is still running.
for (const {server: http2Server, channelzRef: ref} of this.http2ServerList) {
if (http2Server.listening) {
http2Server.close(() => {
this.listenerChildrenTracker.unrefChild(ref);
unregisterChannelzRef(ref);
});
}
}
this.started = false;
// Always destroy any available sessions. It's possible that one or more
// tryShutdown() calls are in progress. Don't wait on them to finish.
this.sessions.forEach((channelzInfo, session) => {
// Cast NGHTTP2_CANCEL to any because TypeScript doesn't seem to
// recognize destroy(code) as a valid signature.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
session.destroy(http2.constants.NGHTTP2_CANCEL as any);
});
this.sessions.clear();
unregisterChannelzRef(this.channelzRef);
}
register<RequestType, ResponseType>(
name: string,
handler: HandleCall<RequestType, ResponseType>,
serialize: Serialize<ResponseType>,
deserialize: Deserialize<RequestType>,
type: string
): boolean {
if (this.handlers.has(name)) {
return false;
}
this.handlers.set(name, {
func: handler,
serialize,
deserialize,
type,
path: name,
} as UntypedHandler);
return true;
}
unregister(name: string): boolean {
return this.handlers.delete(name);
}
start(): void {
if (
this.http2ServerList.length === 0 ||
this.http2ServerList.every(
({server: http2Server}) => http2Server.listening !== true
)
) {
throw new Error('server must be bound in order to start');
}
if (this.started === true) {
throw new Error('server is already started');
}
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Starting');
}
this.started = true;
}
tryShutdown(callback: (error?: Error) => void): void {
const wrappedCallback = (error?: Error) => {
unregisterChannelzRef(this.channelzRef);
callback(error);
};
let pendingChecks = 0;
function maybeCallback(): void {
pendingChecks--;
if (pendingChecks === 0) {
wrappedCallback();
}
}
// Close the server if necessary.
this.started = false;
for (const {server: http2Server, channelzRef: ref} of this.http2ServerList) {
if (http2Server.listening) {
pendingChecks++;
http2Server.close(() => {
this.listenerChildrenTracker.unrefChild(ref);
unregisterChannelzRef(ref);
maybeCallback();
});
}
}
this.sessions.forEach((channelzInfo, session) => {
if (!session.closed) {
pendingChecks += 1;
session.close(maybeCallback);
}
});
if (pendingChecks === 0) {
wrappedCallback();
}
}
addHttp2Port(): void {
throw new Error('Not yet implemented');
}
/**
* Get the channelz reference object for this server. The returned value is
* garbage if channelz is disabled for this server.
* @returns
*/
getChannelzRef() {
return this.channelzRef;
}
private _setupHandlers(
http2Server: http2.Http2Server | http2.Http2SecureServer
): void {
if (http2Server === null) {
return;
}
http2Server.on(
'stream',
(stream: http2.ServerHttp2Stream, headers: http2.IncomingHttpHeaders) => {
const channelzSessionInfo = this.sessions.get(stream.session as http2.ServerHttp2Session);
this.callTracker.addCallStarted();
channelzSessionInfo?.streamTracker.addCallStarted();
const contentType = headers[http2.constants.HTTP2_HEADER_CONTENT_TYPE];
if (
typeof contentType !== 'string' ||
!contentType.startsWith('application/grpc')
) {
stream.respond(
{
[http2.constants.HTTP2_HEADER_STATUS]:
http2.constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
},
{ endStream: true }
);
this.callTracker.addCallFailed();
channelzSessionInfo?.streamTracker.addCallFailed();
return;
}
let call: Http2ServerCallStream<any, any> | null = null;
try {
const path = headers[http2.constants.HTTP2_HEADER_PATH] as string;
const serverAddress = http2Server.address();
let serverAddressString = 'null';
if (serverAddress) {
if (typeof serverAddress === 'string') {
serverAddressString = serverAddress;
} else {
serverAddressString =
serverAddress.address + ':' + serverAddress.port;
}
}
this.trace(
'Received call to method ' +
path +
' at address ' +
serverAddressString
);
const handler = this.handlers.get(path);
if (handler === undefined) {
this.trace(
'No handler registered for method ' +
path +
'. Sending UNIMPLEMENTED status.'
);
throw getUnimplementedStatusResponse(path);
}
call = new Http2ServerCallStream(stream, handler, this.options);
call.once('callEnd', (code: Status) => {
if (code === Status.OK) {
this.callTracker.addCallSucceeded();
} else {
this.callTracker.addCallFailed();
}
});
if (channelzSessionInfo) {
call.once('streamEnd', (success: boolean) => {
if (success) {
channelzSessionInfo.streamTracker.addCallSucceeded();
} else {
channelzSessionInfo.streamTracker.addCallFailed();
}
});
call.on('sendMessage', () => {
channelzSessionInfo.messagesSent += 1;
channelzSessionInfo.lastMessageSentTimestamp = new Date();
});
call.on('receiveMessage', () => {
channelzSessionInfo.messagesReceived += 1;
channelzSessionInfo.lastMessageReceivedTimestamp = new Date();
});
}
const metadata: Metadata = call.receiveMetadata(headers) as Metadata;
switch (handler.type) {
case 'unary':
handleUnary(call, handler as UntypedUnaryHandler, metadata);
break;
case 'clientStream':
handleClientStreaming(
call,
handler as UntypedClientStreamingHandler,
metadata
);
break;
case 'serverStream':
handleServerStreaming(
call,
handler as UntypedServerStreamingHandler,
metadata
);
break;
case 'bidi':
handleBidiStreaming(
call,
handler as UntypedBidiStreamingHandler,
metadata
);
break;
default:
throw new Error(`Unknown handler type: ${handler.type}`);
}
} catch (err) {
if (!call) {
call = new Http2ServerCallStream(stream, null!, this.options);
this.callTracker.addCallFailed();
channelzSessionInfo?.streamTracker.addCallFailed()
}
if (err.code === undefined) {
err.code = Status.INTERNAL;
}
call.sendError(err);
}
}
);
http2Server.on('session', (session) => {
if (!this.started) {
session.destroy();
return;
}
const channelzRef = registerChannelzSocket(session.socket.remoteAddress ?? 'unknown', this.getChannelzSessionInfoGetter(session));
const channelzSessionInfo: ChannelzSessionInfo = {
ref: channelzRef,
streamTracker: new ChannelzCallTracker(),
messagesSent: 0,
messagesReceived: 0,
lastMessageSentTimestamp: null,
lastMessageReceivedTimestamp: null
};
this.sessions.set(session, channelzSessionInfo);
const clientAddress = session.socket.remoteAddress;
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Connection established by client ' + clientAddress);
this.sessionChildrenTracker.refChild(channelzRef);
}
session.on('close', () => {
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by client ' + clientAddress);
this.sessionChildrenTracker.unrefChild(channelzRef);
unregisterChannelzRef(channelzRef);
}
this.sessions.delete(session);
});
});
}
}
async function handleUnary<RequestType, ResponseType>(
call: Http2ServerCallStream<RequestType, ResponseType>,
handler: UnaryHandler<RequestType, ResponseType>,
metadata: Metadata
): Promise<void> {
const request = await call.receiveUnaryMessage();
if (request === undefined || call.cancelled) {
return;
}
const emitter = new ServerUnaryCallImpl<RequestType, ResponseType>(
call,
metadata,
request
);
handler.func(
emitter,
(
err: ServerErrorResponse | ServerStatusResponse | null,
value?: ResponseType | null,
trailer?: Metadata,
flags?: number
) => {
call.sendUnaryMessage(err, value, trailer, flags);
}
);
}
function handleClientStreaming<RequestType, ResponseType>(
call: Http2ServerCallStream<RequestType, ResponseType>,
handler: ClientStreamingHandler<RequestType, ResponseType>,
metadata: Metadata
): void {
const stream = new ServerReadableStreamImpl<RequestType, ResponseType>(
call,
metadata,
handler.deserialize
);
function respond(
err: ServerErrorResponse | ServerStatusResponse | null,
value?: ResponseType | null,
trailer?: Metadata,
flags?: number
) {
stream.destroy();
call.sendUnaryMessage(err, value, trailer, flags);
}
if (call.cancelled) {
return;
}
stream.on('error', respond);
handler.func(stream, respond);
}
async function handleServerStreaming<RequestType, ResponseType>(
call: Http2ServerCallStream<RequestType, ResponseType>,
handler: ServerStreamingHandler<RequestType, ResponseType>,
metadata: Metadata
): Promise<void> {
const request = await call.receiveUnaryMessage();
if (request === undefined || call.cancelled) {
return;
}
const stream = new ServerWritableStreamImpl<RequestType, ResponseType>(
call,
metadata,
handler.serialize,
request
);
handler.func(stream);
}
function handleBidiStreaming<RequestType, ResponseType>(
call: Http2ServerCallStream<RequestType, ResponseType>,
handler: BidiStreamingHandler<RequestType, ResponseType>,
metadata: Metadata
): void {
const stream = new ServerDuplexStreamImpl<RequestType, ResponseType>(
call,
metadata,
handler.serialize,
handler.deserialize
);
if (call.cancelled) {
return;
}
handler.func(stream);
}
+338
View File
@@ -0,0 +1,338 @@
/*
* 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.
*
*/
/* This file implements gRFC A2 and the service config spec:
* https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
* https://github.com/grpc/grpc/blob/master/doc/service_config.md. Each
* function here takes an object with unknown structure and returns its
* specific object type if the input has the right structure, and throws an
* error otherwise. */
/* The any type is purposely used here. All functions validate their input at
* runtime */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as os from 'os';
import {
LoadBalancingConfig,
validateLoadBalancingConfig,
} from './load-balancer';
export interface MethodConfigName {
service: string;
method?: string;
}
export interface Duration {
seconds: number;
nanos: number;
}
export interface MethodConfig {
name: MethodConfigName[];
waitForReady?: boolean;
timeout?: Duration;
maxRequestBytes?: number;
maxResponseBytes?: number;
}
export interface ServiceConfig {
loadBalancingPolicy?: string;
loadBalancingConfig: LoadBalancingConfig[];
methodConfig: MethodConfig[];
}
export interface ServiceConfigCanaryConfig {
clientLanguage?: string[];
percentage?: number;
clientHostname?: string[];
serviceConfig: ServiceConfig;
}
/**
* Recognizes a number with up to 9 digits after the decimal point, followed by
* an "s", representing a number of seconds.
*/
const TIMEOUT_REGEX = /^\d+(\.\d{1,9})?s$/;
/**
* Client language name used for determining whether this client matches a
* `ServiceConfigCanaryConfig`'s `clientLanguage` list.
*/
const CLIENT_LANGUAGE_STRING = 'node';
function validateName(obj: any): MethodConfigName {
if (!('service' in obj) || typeof obj.service !== 'string') {
throw new Error('Invalid method config name: invalid service');
}
const result: MethodConfigName = {
service: obj.service,
};
if ('method' in obj) {
if (typeof obj.method === 'string') {
result.method = obj.method;
} else {
throw new Error('Invalid method config name: invalid method');
}
}
return result;
}
function validateMethodConfig(obj: any): MethodConfig {
const result: MethodConfig = {
name: [],
};
if (!('name' in obj) || !Array.isArray(obj.name)) {
throw new Error('Invalid method config: invalid name array');
}
for (const name of obj.name) {
result.name.push(validateName(name));
}
if ('waitForReady' in obj) {
if (typeof obj.waitForReady !== 'boolean') {
throw new Error('Invalid method config: invalid waitForReady');
}
result.waitForReady = obj.waitForReady;
}
if ('timeout' in obj) {
if (typeof obj.timeout === 'object') {
if (
!('seconds' in obj.timeout) ||
!(typeof obj.timeout.seconds === 'number')
) {
throw new Error('Invalid method config: invalid timeout.seconds');
}
if (
!('nanos' in obj.timeout) ||
!(typeof obj.timeout.nanos === 'number')
) {
throw new Error('Invalid method config: invalid timeout.nanos');
}
result.timeout = obj.timeout;
} else if (
typeof obj.timeout === 'string' &&
TIMEOUT_REGEX.test(obj.timeout)
) {
const timeoutParts = obj.timeout
.substring(0, obj.timeout.length - 1)
.split('.');
result.timeout = {
seconds: timeoutParts[0] | 0,
nanos: (timeoutParts[1] ?? 0) | 0,
};
} else {
throw new Error('Invalid method config: invalid timeout');
}
}
if ('maxRequestBytes' in obj) {
if (typeof obj.maxRequestBytes !== 'number') {
throw new Error('Invalid method config: invalid maxRequestBytes');
}
result.maxRequestBytes = obj.maxRequestBytes;
}
if ('maxResponseBytes' in obj) {
if (typeof obj.maxResponseBytes !== 'number') {
throw new Error('Invalid method config: invalid maxRequestBytes');
}
result.maxResponseBytes = obj.maxResponseBytes;
}
return result;
}
export function validateServiceConfig(obj: any): ServiceConfig {
const result: ServiceConfig = {
loadBalancingConfig: [],
methodConfig: [],
};
if ('loadBalancingPolicy' in obj) {
if (typeof obj.loadBalancingPolicy === 'string') {
result.loadBalancingPolicy = obj.loadBalancingPolicy;
} else {
throw new Error('Invalid service config: invalid loadBalancingPolicy');
}
}
if ('loadBalancingConfig' in obj) {
if (Array.isArray(obj.loadBalancingConfig)) {
for (const config of obj.loadBalancingConfig) {
result.loadBalancingConfig.push(validateLoadBalancingConfig(config));
}
} else {
throw new Error('Invalid service config: invalid loadBalancingConfig');
}
}
if ('methodConfig' in obj) {
if (Array.isArray(obj.methodConfig)) {
for (const methodConfig of obj.methodConfig) {
result.methodConfig.push(validateMethodConfig(methodConfig));
}
}
}
// Validate method name uniqueness
const seenMethodNames: MethodConfigName[] = [];
for (const methodConfig of result.methodConfig) {
for (const name of methodConfig.name) {
for (const seenName of seenMethodNames) {
if (
name.service === seenName.service &&
name.method === seenName.method
) {
throw new Error(
`Invalid service config: duplicate name ${name.service}/${name.method}`
);
}
}
seenMethodNames.push(name);
}
}
return result;
}
function validateCanaryConfig(obj: any): ServiceConfigCanaryConfig {
if (!('serviceConfig' in obj)) {
throw new Error('Invalid service config choice: missing service config');
}
const result: ServiceConfigCanaryConfig = {
serviceConfig: validateServiceConfig(obj.serviceConfig),
};
if ('clientLanguage' in obj) {
if (Array.isArray(obj.clientLanguage)) {
result.clientLanguage = [];
for (const lang of obj.clientLanguage) {
if (typeof lang === 'string') {
result.clientLanguage.push(lang);
} else {
throw new Error(
'Invalid service config choice: invalid clientLanguage'
);
}
}
} else {
throw new Error('Invalid service config choice: invalid clientLanguage');
}
}
if ('clientHostname' in obj) {
if (Array.isArray(obj.clientHostname)) {
result.clientHostname = [];
for (const lang of obj.clientHostname) {
if (typeof lang === 'string') {
result.clientHostname.push(lang);
} else {
throw new Error(
'Invalid service config choice: invalid clientHostname'
);
}
}
} else {
throw new Error('Invalid service config choice: invalid clientHostname');
}
}
if ('percentage' in obj) {
if (
typeof obj.percentage === 'number' &&
0 <= obj.percentage &&
obj.percentage <= 100
) {
result.percentage = obj.percentage;
} else {
throw new Error('Invalid service config choice: invalid percentage');
}
}
// Validate that no unexpected fields are present
const allowedFields = [
'clientLanguage',
'percentage',
'clientHostname',
'serviceConfig',
];
for (const field in obj) {
if (!allowedFields.includes(field)) {
throw new Error(
`Invalid service config choice: unexpected field ${field}`
);
}
}
return result;
}
function validateAndSelectCanaryConfig(
obj: any,
percentage: number
): ServiceConfig {
if (!Array.isArray(obj)) {
throw new Error('Invalid service config list');
}
for (const config of obj) {
const validatedConfig = validateCanaryConfig(config);
/* For each field, we check if it is present, then only discard the
* config if the field value does not match the current client */
if (
typeof validatedConfig.percentage === 'number' &&
percentage > validatedConfig.percentage
) {
continue;
}
if (Array.isArray(validatedConfig.clientHostname)) {
let hostnameMatched = false;
for (const hostname of validatedConfig.clientHostname) {
if (hostname === os.hostname()) {
hostnameMatched = true;
}
}
if (!hostnameMatched) {
continue;
}
}
if (Array.isArray(validatedConfig.clientLanguage)) {
let languageMatched = false;
for (const language of validatedConfig.clientLanguage) {
if (language === CLIENT_LANGUAGE_STRING) {
languageMatched = true;
}
}
if (!languageMatched) {
continue;
}
}
return validatedConfig.serviceConfig;
}
throw new Error('No matching service config found');
}
/**
* Find the "grpc_config" record among the TXT records, parse its value as JSON, validate its contents,
* and select a service config with selection fields that all match this client. Most of these steps
* can fail with an error; the caller must handle any errors thrown this way.
* @param txtRecord The TXT record array that is output from a successful call to dns.resolveTxt
* @param percentage A number chosen from the range [0, 100) that is used to select which config to use
* @return The service configuration to use, given the percentage value, or null if the service config
* data has a valid format but none of the options match the current client.
*/
export function extractAndSelectServiceConfig(
txtRecord: string[][],
percentage: number
): ServiceConfig | null {
for (const record of txtRecord) {
if (record.length > 0 && record[0].startsWith('grpc_config=')) {
/* Treat the list of strings in this record as a single string and remove
* "grpc_config=" from the beginning. The rest should be a JSON string */
const recordString = record.join('').substring('grpc_config='.length);
const recordJson: any = JSON.parse(recordString);
return validateAndSelectCanaryConfig(recordJson, percentage);
}
}
return null;
}
+80
View File
@@ -0,0 +1,80 @@
/*
* 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 { StatusObject } from './call-stream';
import { Status } from './constants';
import { Metadata } from './metadata';
/**
* A builder for gRPC status objects.
*/
export class StatusBuilder {
private code: Status | null;
private details: string | null;
private metadata: Metadata | null;
constructor() {
this.code = null;
this.details = null;
this.metadata = null;
}
/**
* Adds a status code to the builder.
*/
withCode(code: Status): this {
this.code = code;
return this;
}
/**
* Adds details to the builder.
*/
withDetails(details: string): this {
this.details = details;
return this;
}
/**
* Adds metadata to the builder.
*/
withMetadata(metadata: Metadata): this {
this.metadata = metadata;
return this;
}
/**
* Builds the status object.
*/
build(): Partial<StatusObject> {
const status: Partial<StatusObject> = {};
if (this.code !== null) {
status.code = this.code;
}
if (this.details !== null) {
status.details = this.details;
}
if (this.metadata !== null) {
status.metadata = this.metadata;
}
return status;
}
}
+105
View File
@@ -0,0 +1,105 @@
/*
* 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.
*
*/
enum ReadState {
NO_DATA,
READING_SIZE,
READING_MESSAGE,
}
export class StreamDecoder {
private readState: ReadState = ReadState.NO_DATA;
private readCompressFlag: Buffer = Buffer.alloc(1);
private readPartialSize: Buffer = Buffer.alloc(4);
private readSizeRemaining = 4;
private readMessageSize = 0;
private readPartialMessage: Buffer[] = [];
private readMessageRemaining = 0;
write(data: Buffer): Buffer[] {
let readHead = 0;
let toRead: number;
const result: Buffer[] = [];
while (readHead < data.length) {
switch (this.readState) {
case ReadState.NO_DATA:
this.readCompressFlag = data.slice(readHead, readHead + 1);
readHead += 1;
this.readState = ReadState.READING_SIZE;
this.readPartialSize.fill(0);
this.readSizeRemaining = 4;
this.readMessageSize = 0;
this.readMessageRemaining = 0;
this.readPartialMessage = [];
break;
case ReadState.READING_SIZE:
toRead = Math.min(data.length - readHead, this.readSizeRemaining);
data.copy(
this.readPartialSize,
4 - this.readSizeRemaining,
readHead,
readHead + toRead
);
this.readSizeRemaining -= toRead;
readHead += toRead;
// readSizeRemaining >=0 here
if (this.readSizeRemaining === 0) {
this.readMessageSize = this.readPartialSize.readUInt32BE(0);
this.readMessageRemaining = this.readMessageSize;
if (this.readMessageRemaining > 0) {
this.readState = ReadState.READING_MESSAGE;
} else {
const message = Buffer.concat(
[this.readCompressFlag, this.readPartialSize],
5
);
this.readState = ReadState.NO_DATA;
result.push(message);
}
}
break;
case ReadState.READING_MESSAGE:
toRead = Math.min(data.length - readHead, this.readMessageRemaining);
this.readPartialMessage.push(data.slice(readHead, readHead + toRead));
this.readMessageRemaining -= toRead;
readHead += toRead;
// readMessageRemaining >=0 here
if (this.readMessageRemaining === 0) {
// At this point, we have read a full message
const framedMessageBuffers = [
this.readCompressFlag,
this.readPartialSize,
].concat(this.readPartialMessage);
const framedMessage = Buffer.concat(
framedMessageBuffers,
this.readMessageSize + 5
);
this.readState = ReadState.NO_DATA;
result.push(framedMessage);
}
break;
default:
throw new Error('Unexpected read state');
}
}
return result;
}
}
+79
View File
@@ -0,0 +1,79 @@
/*
* Copyright 2021 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 { isIP } from "net";
export interface TcpSubchannelAddress {
port: number;
host: string;
}
export interface IpcSubchannelAddress {
path: string;
}
/**
* This represents a single backend address to connect to. This interface is a
* subset of net.SocketConnectOpts, i.e. the options described at
* https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener.
* Those are in turn a subset of the options that can be passed to http2.connect.
*/
export type SubchannelAddress = TcpSubchannelAddress | IpcSubchannelAddress;
export function isTcpSubchannelAddress(
address: SubchannelAddress
): address is TcpSubchannelAddress {
return 'port' in address;
}
export function subchannelAddressEqual(
address1: SubchannelAddress,
address2: SubchannelAddress
): boolean {
if (isTcpSubchannelAddress(address1)) {
return (
isTcpSubchannelAddress(address2) &&
address1.host === address2.host &&
address1.port === address2.port
);
} else {
return !isTcpSubchannelAddress(address2) && address1.path === address2.path;
}
}
export function subchannelAddressToString(address: SubchannelAddress): string {
if (isTcpSubchannelAddress(address)) {
return address.host + ':' + address.port;
} else {
return address.path;
}
}
const DEFAULT_PORT = 443;
export function stringToSubchannelAddress(addressString: string, port?: number): SubchannelAddress {
if (isIP(addressString)) {
return {
host: addressString,
port: port ?? DEFAULT_PORT
};
} else {
return {
path: addressString
};
}
}
+178
View File
@@ -0,0 +1,178 @@
/*
* 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 { ChannelOptions, channelOptionsEqual } from './channel-options';
import { Subchannel } from './subchannel';
import {
SubchannelAddress,
subchannelAddressEqual,
} from './subchannel-address';
import { ChannelCredentials } from './channel-credentials';
import { GrpcUri, uriToString } from './uri-parser';
// 10 seconds in milliseconds. This value is arbitrary.
/**
* The amount of time in between checks for dropping subchannels that have no
* other references
*/
const REF_CHECK_INTERVAL = 10_000;
export class SubchannelPool {
private pool: {
[channelTarget: string]: Array<{
subchannelAddress: SubchannelAddress;
channelArguments: ChannelOptions;
channelCredentials: ChannelCredentials;
subchannel: Subchannel;
}>;
} = Object.create(null);
/**
* A timer of a task performing a periodic subchannel cleanup.
*/
private cleanupTimer: NodeJS.Timer | null = null;
/**
* A pool of subchannels use for making connections. Subchannels with the
* exact same parameters will be reused.
* @param global If true, this is the global subchannel pool. Otherwise, it
* is the pool for a single channel.
*/
constructor(private global: boolean) {}
/**
* Unrefs all unused subchannels and cancels the cleanup task if all
* subchannels have been unrefed.
*/
unrefUnusedSubchannels(): void {
let allSubchannelsUnrefed = true;
/* These objects are created with Object.create(null), so they do not
* have a prototype, which means that for (... in ...) loops over them
* do not need to be filtered */
// eslint-disable-disable-next-line:forin
for (const channelTarget in this.pool) {
const subchannelObjArray = this.pool[channelTarget];
const refedSubchannels = subchannelObjArray.filter(
(value) => !value.subchannel.unrefIfOneRef()
);
if (refedSubchannels.length > 0) {
allSubchannelsUnrefed = false;
}
/* For each subchannel in the pool, try to unref it if it has
* exactly one ref (which is the ref from the pool itself). If that
* does happen, remove the subchannel from the pool */
this.pool[channelTarget] = refedSubchannels;
}
/* Currently we do not delete keys with empty values. If that results
* in significant memory usage we should change it. */
// Cancel the cleanup task if all subchannels have been unrefed.
if (allSubchannelsUnrefed && this.cleanupTimer !== null) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
}
/**
* Ensures that the cleanup task is spawned.
*/
ensureCleanupTask(): void {
if (this.global && this.cleanupTimer === null) {
this.cleanupTimer = setInterval(() => {
this.unrefUnusedSubchannels();
}, REF_CHECK_INTERVAL);
// Unref because this timer should not keep the event loop running.
// Call unref only if it exists to address electron/electron#21162
this.cleanupTimer.unref?.();
}
}
/**
* Get a subchannel if one already exists with exactly matching parameters.
* Otherwise, create and save a subchannel with those parameters.
* @param channelTarget
* @param subchannelTarget
* @param channelArguments
* @param channelCredentials
*/
getOrCreateSubchannel(
channelTargetUri: GrpcUri,
subchannelTarget: SubchannelAddress,
channelArguments: ChannelOptions,
channelCredentials: ChannelCredentials
): Subchannel {
this.ensureCleanupTask();
const channelTarget = uriToString(channelTargetUri);
if (channelTarget in this.pool) {
const subchannelObjArray = this.pool[channelTarget];
for (const subchannelObj of subchannelObjArray) {
if (
subchannelAddressEqual(
subchannelTarget,
subchannelObj.subchannelAddress
) &&
channelOptionsEqual(
channelArguments,
subchannelObj.channelArguments
) &&
channelCredentials._equals(subchannelObj.channelCredentials)
) {
return subchannelObj.subchannel;
}
}
}
// If we get here, no matching subchannel was found
const subchannel = new Subchannel(
channelTargetUri,
subchannelTarget,
channelArguments,
channelCredentials
);
if (!(channelTarget in this.pool)) {
this.pool[channelTarget] = [];
}
this.pool[channelTarget].push({
subchannelAddress: subchannelTarget,
channelArguments,
channelCredentials,
subchannel,
});
if (this.global) {
subchannel.ref();
}
return subchannel;
}
}
const globalSubchannelPool = new SubchannelPool(true);
/**
* Get either the global subchannel pool, or a new subchannel pool.
* @param global
*/
export function getSubchannelPool(global: boolean): SubchannelPool {
if (global) {
return globalSubchannelPool;
} else {
return new SubchannelPool(false);
}
}
+952
View File
@@ -0,0 +1,952 @@
/*
* 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 * as http2 from 'http2';
import { ChannelCredentials } from './channel-credentials';
import { Metadata } from './metadata';
import { Call, Http2CallStream, WriteObject } from './call-stream';
import { ChannelOptions } from './channel-options';
import { PeerCertificate, checkServerIdentity, TLSSocket, CipherNameAndProtocol } from 'tls';
import { ConnectivityState } from './connectivity-state';
import { BackoffTimeout, BackoffOptions } from './backoff-timeout';
import { getDefaultAuthority } from './resolver';
import * as logging from './logging';
import { LogVerbosity, Status } from './constants';
import { getProxiedConnection, ProxyConnectionResult } from './http_proxy';
import * as net from 'net';
import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
import { ConnectionOptions } from 'tls';
import { FilterFactory, Filter, BaseFilter } from './filter';
import {
stringToSubchannelAddress,
SubchannelAddress,
subchannelAddressToString,
} from './subchannel-address';
import { SubchannelRef, ChannelzTrace, ChannelzChildrenTracker, SubchannelInfo, registerChannelzSubchannel, ChannelzCallTracker, SocketInfo, SocketRef, unregisterChannelzRef, registerChannelzSocket, TlsInfo } from './channelz';
const clientVersion = require('../../package.json').version;
const TRACER_NAME = 'subchannel';
const MIN_CONNECT_TIMEOUT_MS = 20000;
const INITIAL_BACKOFF_MS = 1000;
const BACKOFF_MULTIPLIER = 1.6;
const MAX_BACKOFF_MS = 120000;
const BACKOFF_JITTER = 0.2;
/* 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);
const KEEPALIVE_TIMEOUT_MS = 20000;
export type ConnectivityStateListener = (
subchannel: Subchannel,
previousState: ConnectivityState,
newState: ConnectivityState
) => void;
export interface SubchannelCallStatsTracker {
addMessageSent(): void;
addMessageReceived(): void;
}
const {
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
HTTP2_HEADER_TE,
HTTP2_HEADER_USER_AGENT,
} = http2.constants;
/**
* Get a number uniformly at random in the range [min, max)
* @param min
* @param max
*/
function uniformRandom(min: number, max: number) {
return Math.random() * (max - min) + min;
}
const tooManyPingsData: Buffer = Buffer.from('too_many_pings', 'ascii');
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 session: http2.ClientHttp2Session | 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: ConnectivityStateListener[] = [];
/**
* A list of listener functions that will be called when the underlying
* socket disconnects. Used for ending active calls with an UNAVAILABLE
* status.
*/
private disconnectListeners: Array<() => void> = [];
private backoffTimeout: BackoffTimeout;
/**
* The complete user agent string constructed using channel args.
*/
private userAgent: string;
/**
* The amount of time in between sending pings
*/
private keepaliveTimeMs: number = KEEPALIVE_MAX_TIME_MS;
/**
* The amount of time to wait for an acknowledgement after sending a ping
*/
private keepaliveTimeoutMs: number = KEEPALIVE_TIMEOUT_MS;
/**
* Timer reference for timeout that indicates when to send the next ping
*/
private keepaliveIntervalId: NodeJS.Timer;
/**
* Timer reference tracking when the most recent ping will be considered lost
*/
private keepaliveTimeoutId: NodeJS.Timer;
/**
* Indicates whether keepalive pings should be sent without any active calls
*/
private keepaliveWithoutCalls = false;
/**
* Tracks calls with references to this subchannel
*/
private callRefcount = 0;
/**
* 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 channelzSocketRef: SocketRef | null = null;
/**
* Name of the remote server, if it is not the same as the subchannel
* address, i.e. if connecting through an HTTP CONNECT proxy.
*/
private remoteName: string | null = null;
private streamTracker = new ChannelzCallTracker();
private keepalivesSent = 0;
private messagesSent = 0;
private messagesReceived = 0;
private lastMessageSentTimestamp: Date | null = null;
private lastMessageReceivedTimestamp: Date | null = null;
/**
* 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
) {
// Build user-agent string.
this.userAgent = [
options['grpc.primary_user_agent'],
`grpc-node-js/${clientVersion}`,
options['grpc.secondary_user_agent'],
]
.filter((e) => e)
.join(' '); // remove falsey values first
if ('grpc.keepalive_time_ms' in options) {
this.keepaliveTimeMs = options['grpc.keepalive_time_ms']!;
}
if ('grpc.keepalive_timeout_ms' in options) {
this.keepaliveTimeoutMs = options['grpc.keepalive_timeout_ms']!;
}
if ('grpc.keepalive_permit_without_calls' in options) {
this.keepaliveWithoutCalls =
options['grpc.keepalive_permit_without_calls'] === 1;
} else {
this.keepaliveWithoutCalls = false;
}
this.keepaliveIntervalId = setTimeout(() => {}, 0);
clearTimeout(this.keepaliveIntervalId);
this.keepaliveTimeoutId = setTimeout(() => {}, 0);
clearTimeout(this.keepaliveTimeoutId);
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);
if (options['grpc.enable_channelz'] === 0) {
this.channelzEnabled = false;
}
this.channelzTrace = new ChannelzTrace();
if (this.channelzEnabled) {
this.channelzRef = registerChannelzSubchannel(this.subchannelAddressString, () => this.getChannelzInfo());
this.channelzTrace.addTrace('CT_INFO', 'Subchannel created');
} else {
// Dummy channelz ref that will never be used
this.channelzRef = {
kind: 'subchannel',
id: -1,
name: ''
};
}
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 getChannelzSocketInfo(): SocketInfo | null {
if (this.session === null) {
return null;
}
const sessionSocket = this.session.socket;
const remoteAddress = sessionSocket.remoteAddress ? stringToSubchannelAddress(sessionSocket.remoteAddress, sessionSocket.remotePort) : null;
const localAddress = sessionSocket.localAddress ? stringToSubchannelAddress(sessionSocket.localAddress, sessionSocket.localPort) : null;
let tlsInfo: TlsInfo | null;
if (this.session.encrypted) {
const tlsSocket: TLSSocket = sessionSocket as TLSSocket;
const cipherInfo: CipherNameAndProtocol & {standardName?: string} = tlsSocket.getCipher();
const certificate = tlsSocket.getCertificate();
const peerCertificate = tlsSocket.getPeerCertificate();
tlsInfo = {
cipherSuiteStandardName: cipherInfo.standardName ?? null,
cipherSuiteOtherName: cipherInfo.standardName ? null : cipherInfo.name,
localCertificate: (certificate && 'raw' in certificate) ? certificate.raw : null,
remoteCertificate: (peerCertificate && 'raw' in peerCertificate) ? peerCertificate.raw : null
};
} else {
tlsInfo = null;
}
const socketInfo: SocketInfo = {
remoteAddress: remoteAddress,
localAddress: localAddress,
security: tlsInfo,
remoteName: this.remoteName,
streamsStarted: this.streamTracker.callsStarted,
streamsSucceeded: this.streamTracker.callsSucceeded,
streamsFailed: this.streamTracker.callsFailed,
messagesSent: this.messagesSent,
messagesReceived: this.messagesReceived,
keepAlivesSent: this.keepalivesSent,
lastLocalStreamCreatedTimestamp: this.streamTracker.lastCallStartedTimestamp,
lastRemoteStreamCreatedTimestamp: null,
lastMessageSentTimestamp: this.lastMessageSentTimestamp,
lastMessageReceivedTimestamp: this.lastMessageReceivedTimestamp,
localFlowControlWindow: this.session.state.localWindowSize ?? null,
remoteFlowControlWindow: this.session.state.remoteWindowSize ?? null
};
return socketInfo;
}
private resetChannelzSocketInfo() {
if (!this.channelzEnabled) {
return;
}
if (this.channelzSocketRef) {
unregisterChannelzRef(this.channelzSocketRef);
this.childrenTracker.unrefChild(this.channelzSocketRef);
this.channelzSocketRef = null;
}
this.remoteName = null;
this.streamTracker = new ChannelzCallTracker();
this.keepalivesSent = 0;
this.messagesSent = 0;
this.messagesReceived = 0;
this.lastMessageSentTimestamp = null;
this.lastMessageReceivedTimestamp = null;
}
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 sendPing() {
if (this.channelzEnabled) {
this.keepalivesSent += 1;
}
logging.trace(
LogVerbosity.DEBUG,
'keepalive',
'(' + this.channelzRef.id + ') ' + this.subchannelAddressString + ' ' +
'Sending ping'
);
this.keepaliveTimeoutId = setTimeout(() => {
this.transitionToState([ConnectivityState.READY], ConnectivityState.IDLE);
}, this.keepaliveTimeoutMs);
this.keepaliveTimeoutId.unref?.();
this.session!.ping(
(err: Error | null, duration: number, payload: Buffer) => {
clearTimeout(this.keepaliveTimeoutId);
}
);
}
private startKeepalivePings() {
this.keepaliveIntervalId = setInterval(() => {
this.sendPing();
}, this.keepaliveTimeMs);
this.keepaliveIntervalId.unref?.();
/* Don't send a ping immediately because whatever caused us to start
* sending pings should also involve some network activity. */
}
private stopKeepalivePings() {
clearInterval(this.keepaliveIntervalId);
clearTimeout(this.keepaliveTimeoutId);
}
private createSession(proxyConnectionResult: ProxyConnectionResult) {
if (proxyConnectionResult.realTarget) {
this.remoteName = uriToString(proxyConnectionResult.realTarget);
this.trace('creating HTTP/2 session through proxy to ' + proxyConnectionResult.realTarget);
} else {
this.remoteName = null;
this.trace('creating HTTP/2 session');
}
const targetAuthority = getDefaultAuthority(
proxyConnectionResult.realTarget ?? this.channelTarget
);
let connectionOptions: http2.SecureClientSessionOptions =
this.credentials._getConnectionOptions() || {};
connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER;
if ('grpc-node.max_session_memory' in this.options) {
connectionOptions.maxSessionMemory = this.options[
'grpc-node.max_session_memory'
];
}
let addressScheme = 'http://';
if ('secureContext' in connectionOptions) {
addressScheme = 'https://';
// If provided, the value of grpc.ssl_target_name_override should be used
// to override the target hostname when checking server identity.
// This option is used for testing only.
if (this.options['grpc.ssl_target_name_override']) {
const sslTargetNameOverride = this.options[
'grpc.ssl_target_name_override'
]!;
connectionOptions.checkServerIdentity = (
host: string,
cert: PeerCertificate
): Error | undefined => {
return checkServerIdentity(sslTargetNameOverride, cert);
};
connectionOptions.servername = sslTargetNameOverride;
} else {
const authorityHostname =
splitHostPort(targetAuthority)?.host ?? 'localhost';
// We want to always set servername to support SNI
connectionOptions.servername = authorityHostname;
}
if (proxyConnectionResult.socket) {
/* This is part of the workaround for
* https://github.com/nodejs/node/issues/32922. Without that bug,
* proxyConnectionResult.socket would always be a plaintext socket and
* this would say
* connectionOptions.socket = proxyConnectionResult.socket; */
connectionOptions.createConnection = (authority, option) => {
return proxyConnectionResult.socket!;
};
}
} else {
/* In all but the most recent versions of Node, http2.connect does not use
* the options when establishing plaintext connections, so we need to
* establish that connection explicitly. */
connectionOptions.createConnection = (authority, option) => {
if (proxyConnectionResult.socket) {
return proxyConnectionResult.socket;
} else {
/* net.NetConnectOpts is declared in a way that is more restrictive
* than what net.connect will actually accept, so we use the type
* assertion to work around that. */
return net.connect(this.subchannelAddress);
}
};
}
connectionOptions = {
...connectionOptions,
...this.subchannelAddress,
};
/* http2.connect uses the options here:
* https://github.com/nodejs/node/blob/70c32a6d190e2b5d7b9ff9d5b6a459d14e8b7d59/lib/internal/http2/core.js#L3028-L3036
* The spread operator overides earlier values with later ones, so any port
* or host values in the options will be used rather than any values extracted
* from the first argument. In addition, the path overrides the host and port,
* as documented for plaintext connections here:
* https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
* and for TLS connections here:
* https://nodejs.org/api/tls.html#tls_tls_connect_options_callback. In
* earlier versions of Node, http2.connect passes these options to
* tls.connect but not net.connect, so in the insecure case we still need
* to set the createConnection option above to create the connection
* explicitly. We cannot do that in the TLS case because http2.connect
* passes necessary additional options to tls.connect.
* The first argument just needs to be parseable as a URL and the scheme
* determines whether the connection will be established over TLS or not.
*/
const session = http2.connect(
addressScheme + targetAuthority,
connectionOptions
);
this.session = session;
if (this.channelzEnabled) {
this.channelzSocketRef = registerChannelzSocket(this.subchannelAddressString, () => this.getChannelzSocketInfo()!);
this.childrenTracker.refChild(this.channelzSocketRef);
}
session.unref();
/* For all of these events, check if the session at the time of the event
* is the same one currently attached to this subchannel, to ensure that
* old events from previous connection attempts cannot cause invalid state
* transitions. */
session.once('connect', () => {
if (this.session === session) {
this.transitionToState(
[ConnectivityState.CONNECTING],
ConnectivityState.READY
);
}
});
session.once('close', () => {
if (this.session === session) {
this.trace('connection closed');
this.transitionToState(
[ConnectivityState.CONNECTING],
ConnectivityState.TRANSIENT_FAILURE
);
/* Transitioning directly to IDLE here should be OK because we are not
* doing any backoff, because a connection was established at some
* point */
this.transitionToState(
[ConnectivityState.READY],
ConnectivityState.IDLE
);
}
});
session.once(
'goaway',
(errorCode: number, lastStreamID: number, opaqueData: Buffer) => {
if (this.session === session) {
/* See the last paragraph of
* https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#basic-keepalive */
if (
errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM &&
opaqueData.equals(tooManyPingsData)
) {
this.keepaliveTimeMs = Math.min(
2 * this.keepaliveTimeMs,
KEEPALIVE_MAX_TIME_MS
);
logging.log(
LogVerbosity.ERROR,
`Connection to ${uriToString(this.channelTarget)} at ${
this.subchannelAddressString
} rejected by server because of excess pings. Increasing ping interval to ${
this.keepaliveTimeMs
} ms`
);
}
this.trace(
'connection closed by GOAWAY with code ' +
errorCode
);
this.transitionToState(
[ConnectivityState.CONNECTING, ConnectivityState.READY],
ConnectivityState.IDLE
);
}
}
);
session.once('error', (error) => {
/* Do nothing here. Any error should also trigger a close event, which is
* where we want to handle that. */
this.trace(
'connection closed with error ' +
(error as Error).message
);
});
}
private startConnectingInternal() {
/* Pass connection options through to the proxy so that it's able to
* upgrade it's connection to support tls if needed.
* This is a workaround for https://github.com/nodejs/node/issues/32922
* See https://github.com/grpc/grpc-node/pull/1369 for more info. */
const connectionOptions: ConnectionOptions =
this.credentials._getConnectionOptions() || {};
if ('secureContext' in connectionOptions) {
connectionOptions.ALPNProtocols = ['h2'];
// If provided, the value of grpc.ssl_target_name_override should be used
// to override the target hostname when checking server identity.
// This option is used for testing only.
if (this.options['grpc.ssl_target_name_override']) {
const sslTargetNameOverride = this.options[
'grpc.ssl_target_name_override'
]!;
connectionOptions.checkServerIdentity = (
host: string,
cert: PeerCertificate
): Error | undefined => {
return checkServerIdentity(sslTargetNameOverride, cert);
};
connectionOptions.servername = sslTargetNameOverride;
} else {
if ('grpc.http_connect_target' in this.options) {
/* This is more or less how servername will be set in createSession
* if a connection is successfully established through the proxy.
* If the proxy is not used, these connectionOptions are discarded
* anyway */
const targetPath = getDefaultAuthority(
parseUri(this.options['grpc.http_connect_target'] as string) ?? {
path: 'localhost',
}
);
const hostPort = splitHostPort(targetPath);
connectionOptions.servername = hostPort?.host ?? targetPath;
}
}
}
getProxiedConnection(
this.subchannelAddress,
this.options,
connectionOptions
).then(
(result) => {
this.createSession(result);
},
(reason) => {
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', ConnectivityState[this.connectivityState] + ' -> ' + ConnectivityState[newState]);
}
const previousState = this.connectivityState;
this.connectivityState = newState;
switch (newState) {
case ConnectivityState.READY:
this.stopBackoff();
this.session!.socket.once('close', () => {
for (const listener of this.disconnectListeners) {
listener();
}
});
if (this.keepaliveWithoutCalls) {
this.startKeepalivePings();
}
break;
case ConnectivityState.CONNECTING:
this.startBackoff();
this.startConnectingInternal();
this.continueConnecting = false;
break;
case ConnectivityState.TRANSIENT_FAILURE:
if (this.session) {
this.session.close();
}
this.session = null;
this.resetChannelzSocketInfo();
this.stopKeepalivePings();
/* 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.session) {
this.session.close();
}
this.session = null;
this.resetChannelzSocketInfo();
this.stopKeepalivePings();
break;
default:
throw new Error(`Invalid state: unknown ConnectivityState ${newState}`);
}
/* We use a shallow copy of the stateListeners array in case a listener
* is removed during this iteration */
for (const listener of [...this.stateListeners]) {
listener(this, previousState, newState);
}
return true;
}
/**
* Check if the subchannel associated with zero calls and with zero channels.
* If so, shut it down.
*/
private checkBothRefcounts() {
/* If no calls, channels, or subchannel pools have any more references to
* this subchannel, we can be sure it will never be used again. */
if (this.callRefcount === 0 && this.refcount === 0) {
if (this.channelzEnabled) {
this.channelzTrace.addTrace('CT_INFO', 'Shutting down');
}
this.transitionToState(
[ConnectivityState.CONNECTING, ConnectivityState.READY],
ConnectivityState.TRANSIENT_FAILURE
);
if (this.channelzEnabled) {
unregisterChannelzRef(this.channelzRef);
}
}
}
callRef() {
this.refTrace(
'callRefcount ' +
this.callRefcount +
' -> ' +
(this.callRefcount + 1)
);
if (this.callRefcount === 0) {
if (this.session) {
this.session.ref();
}
this.backoffTimeout.ref();
if (!this.keepaliveWithoutCalls) {
this.startKeepalivePings();
}
}
this.callRefcount += 1;
}
callUnref() {
this.refTrace(
'callRefcount ' +
this.callRefcount +
' -> ' +
(this.callRefcount - 1)
);
this.callRefcount -= 1;
if (this.callRefcount === 0) {
if (this.session) {
this.session.unref();
}
this.backoffTimeout.unref();
if (!this.keepaliveWithoutCalls) {
this.stopKeepalivePings();
}
this.checkBothRefcounts();
}
}
ref() {
this.refTrace(
'refcount ' +
this.refcount +
' -> ' +
(this.refcount + 1)
);
this.refcount += 1;
}
unref() {
this.refTrace(
'refcount ' +
this.refcount +
' -> ' +
(this.refcount - 1)
);
this.refcount -= 1;
this.checkBothRefcounts();
}
unrefIfOneRef(): boolean {
if (this.refcount === 1) {
this.unref();
return true;
}
return false;
}
/**
* Start a stream on the current session with the given `metadata` as headers
* and then attach it to the `callStream`. Must only be called if the
* subchannel's current connectivity state is READY.
* @param metadata
* @param callStream
*/
startCallStream(
metadata: Metadata,
callStream: Http2CallStream,
extraFilters: Filter[]
) {
const headers = metadata.toHttp2Headers();
headers[HTTP2_HEADER_AUTHORITY] = callStream.getHost();
headers[HTTP2_HEADER_USER_AGENT] = this.userAgent;
headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc';
headers[HTTP2_HEADER_METHOD] = 'POST';
headers[HTTP2_HEADER_PATH] = callStream.getMethod();
headers[HTTP2_HEADER_TE] = 'trailers';
let http2Stream: http2.ClientHttp2Stream;
/* In theory, if an error is thrown by session.request because session has
* become unusable (e.g. because it has received a goaway), this subchannel
* should soon see the corresponding close or goaway event anyway and leave
* READY. But we have seen reports that this does not happen
* (https://github.com/googleapis/nodejs-firestore/issues/1023#issuecomment-653204096)
* so for defense in depth, we just discard the session when we see an
* error here.
*/
try {
http2Stream = this.session!.request(headers);
} catch (e) {
this.transitionToState(
[ConnectivityState.READY],
ConnectivityState.TRANSIENT_FAILURE
);
throw e;
}
let headersString = '';
for (const header of Object.keys(headers)) {
headersString += '\t\t' + header + ': ' + headers[header] + '\n';
}
logging.trace(
LogVerbosity.DEBUG,
'call_stream',
'Starting stream on subchannel ' +
'(' + this.channelzRef.id + ') ' +
this.subchannelAddressString +
' with headers\n' +
headersString
);
const streamSession = this.session;
let statsTracker: SubchannelCallStatsTracker;
if (this.channelzEnabled) {
this.callTracker.addCallStarted();
callStream.addStatusWatcher(status => {
if (status.code === Status.OK) {
this.callTracker.addCallSucceeded();
} else {
this.callTracker.addCallFailed();
}
});
this.streamTracker.addCallStarted();
callStream.addStreamEndWatcher(success => {
if (streamSession === this.session) {
if (success) {
this.streamTracker.addCallSucceeded();
} else {
this.streamTracker.addCallFailed();
}
}
});
statsTracker = {
addMessageSent: () => {
this.messagesSent += 1;
this.lastMessageSentTimestamp = new Date();
},
addMessageReceived: () => {
this.messagesReceived += 1;
}
}
} else {
statsTracker = {
addMessageSent: () => {},
addMessageReceived: () => {}
}
}
callStream.attachHttp2Stream(http2Stream, this, extraFilters, 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() {
/* 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.push(listener);
}
/**
* Remove a listener previously added with `addConnectivityStateListener`
* @param listener A reference to a function previously passed to
* `addConnectivityStateListener`
*/
removeConnectivityStateListener(listener: ConnectivityStateListener) {
const listenerIndex = this.stateListeners.indexOf(listener);
if (listenerIndex > -1) {
this.stateListeners.splice(listenerIndex, 1);
}
}
addDisconnectListener(listener: () => void) {
this.disconnectListeners.push(listener);
}
removeDisconnectListener(listener: () => void) {
const listenerIndex = this.disconnectListeners.indexOf(listener);
if (listenerIndex > -1) {
this.disconnectListeners.splice(listenerIndex, 1);
}
}
/**
* Reset the backoff timeout, and immediately start connecting if in backoff.
*/
resetBackoff() {
this.backoffTimeout.reset();
this.transitionToState(
[ConnectivityState.TRANSIENT_FAILURE],
ConnectivityState.CONNECTING
);
}
getAddress(): string {
return this.subchannelAddressString;
}
getChannelzRef(): SubchannelRef {
return this.channelzRef;
}
}
+35
View File
@@ -0,0 +1,35 @@
/*
* 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 * as fs from 'fs';
export const CIPHER_SUITES: string | undefined =
process.env.GRPC_SSL_CIPHER_SUITES;
const DEFAULT_ROOTS_FILE_PATH = process.env.GRPC_DEFAULT_SSL_ROOTS_FILE_PATH;
let defaultRootsData: Buffer | null = null;
export function getDefaultRootsData(): Buffer | null {
if (DEFAULT_ROOTS_FILE_PATH) {
if (defaultRootsData === null) {
defaultRootsData = fs.readFileSync(DEFAULT_ROOTS_FILE_PATH);
}
return defaultRootsData;
}
return null;
}
+114
View File
@@ -0,0 +1,114 @@
/*
* Copyright 2020 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.
*
*/
export interface GrpcUri {
scheme?: string;
authority?: string;
path: string;
}
/*
* The groups correspond to URI parts as follows:
* 1. scheme
* 2. authority
* 3. path
*/
const URI_REGEX = /^(?:([A-Za-z0-9+.-]+):)?(?:\/\/([^/]*)\/)?(.+)$/;
export function parseUri(uriString: string): GrpcUri | null {
const parsedUri = URI_REGEX.exec(uriString);
if (parsedUri === null) {
return null;
}
return {
scheme: parsedUri[1],
authority: parsedUri[2],
path: parsedUri[3],
};
}
export interface HostPort {
host: string;
port?: number;
}
const NUMBER_REGEX = /^\d+$/;
export function splitHostPort(path: string): HostPort | null {
if (path.startsWith('[')) {
const hostEnd = path.indexOf(']');
if (hostEnd === -1) {
return null;
}
const host = path.substring(1, hostEnd);
/* Only an IPv6 address should be in bracketed notation, and an IPv6
* address should have at least one colon */
if (host.indexOf(':') === -1) {
return null;
}
if (path.length > hostEnd + 1) {
if (path[hostEnd + 1] === ':') {
const portString = path.substring(hostEnd + 2);
if (NUMBER_REGEX.test(portString)) {
return {
host: host,
port: +portString,
};
} else {
return null;
}
} else {
return null;
}
} else {
return {
host,
};
}
} else {
const splitPath = path.split(':');
/* Exactly one colon means that this is host:port. Zero colons means that
* there is no port. And multiple colons means that this is a bare IPv6
* address with no port */
if (splitPath.length === 2) {
if (NUMBER_REGEX.test(splitPath[1])) {
return {
host: splitPath[0],
port: +splitPath[1],
};
} else {
return null;
}
} else {
return {
host: path,
};
}
}
}
export function uriToString(uri: GrpcUri): string {
let result = '';
if (uri.scheme !== undefined) {
result += uri.scheme + ':';
}
if (uri.authority !== undefined) {
result += '//' + uri.authority + '/';
}
result += uri.path;
return result;
}