mirror of https://github.com/grpc/grpc-node.git
grpc-js: Implement ORCA client-side OOB metrics
This commit is contained in:
parent
7e82de7770
commit
ede914ddbf
|
@ -285,6 +285,8 @@ export {
|
||||||
ServerInterceptor,
|
ServerInterceptor,
|
||||||
} from './server-interceptors';
|
} from './server-interceptors';
|
||||||
|
|
||||||
|
export { ServerMetricRecorder } from './orca';
|
||||||
|
|
||||||
import * as experimental from './experimental';
|
import * as experimental from './experimental';
|
||||||
export { experimental };
|
export { experimental };
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ import {
|
||||||
} from './picker';
|
} from './picker';
|
||||||
import { Endpoint, SubchannelAddress, subchannelAddressToString } from './subchannel-address';
|
import { Endpoint, SubchannelAddress, subchannelAddressToString } from './subchannel-address';
|
||||||
import * as logging from './logging';
|
import * as logging from './logging';
|
||||||
import { LogVerbosity } from './constants';
|
import { LogVerbosity, Status } from './constants';
|
||||||
import {
|
import {
|
||||||
SubchannelInterface,
|
SubchannelInterface,
|
||||||
ConnectivityStateListener,
|
ConnectivityStateListener,
|
||||||
|
@ -44,6 +44,12 @@ import { isTcpSubchannelAddress } from './subchannel-address';
|
||||||
import { isIPv6 } from 'net';
|
import { isIPv6 } from 'net';
|
||||||
import { ChannelOptions } from './channel-options';
|
import { ChannelOptions } from './channel-options';
|
||||||
import { StatusOr, statusOrFromValue } from './call-interface';
|
import { StatusOr, statusOrFromValue } from './call-interface';
|
||||||
|
import { OrcaLoadReport__Output } from './generated/xds/data/orca/v3/OrcaLoadReport';
|
||||||
|
import { OpenRcaServiceClient } from './generated/xds/service/orca/v3/OpenRcaService';
|
||||||
|
import { ClientReadableStream, ServiceError } from './call';
|
||||||
|
import { createOrcaClient } from './orca';
|
||||||
|
import { msToDuration } from './duration';
|
||||||
|
import { BackoffTimeout } from './backoff-timeout';
|
||||||
|
|
||||||
const TRACER_NAME = 'pick_first';
|
const TRACER_NAME = 'pick_first';
|
||||||
|
|
||||||
|
@ -59,6 +65,8 @@ const TYPE_NAME = 'pick_first';
|
||||||
*/
|
*/
|
||||||
const CONNECTION_DELAY_INTERVAL_MS = 250;
|
const CONNECTION_DELAY_INTERVAL_MS = 250;
|
||||||
|
|
||||||
|
export type MetricsListener = (loadReport: OrcaLoadReport__Output) => void;
|
||||||
|
|
||||||
export class PickFirstLoadBalancingConfig implements TypedLoadBalancingConfig {
|
export class PickFirstLoadBalancingConfig implements TypedLoadBalancingConfig {
|
||||||
constructor(private readonly shuffleAddressList: boolean) {}
|
constructor(private readonly shuffleAddressList: boolean) {}
|
||||||
|
|
||||||
|
@ -239,6 +247,13 @@ export class PickFirstLoadBalancer implements LoadBalancer {
|
||||||
|
|
||||||
private latestResolutionNote: string = '';
|
private latestResolutionNote: string = '';
|
||||||
|
|
||||||
|
private metricsListeners: Map<MetricsListener, number> = new Map();
|
||||||
|
private orcaClient: OpenRcaServiceClient | null = null;
|
||||||
|
private metricsCall: ClientReadableStream<OrcaLoadReport__Output> | null = null;
|
||||||
|
private currentMetricsIntervalMs: number = Infinity;
|
||||||
|
private orcaUnsupported = false;
|
||||||
|
private metricsBackoffTimer = new BackoffTimeout(() => this.updateMetricsSubscription());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load balancer that attempts to connect to each backend in the address list
|
* 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
|
* in order, and picks the first one that connects, using it for every
|
||||||
|
@ -336,6 +351,12 @@ export class PickFirstLoadBalancer implements LoadBalancer {
|
||||||
this.currentPick.removeHealthStateWatcher(
|
this.currentPick.removeHealthStateWatcher(
|
||||||
this.pickedSubchannelHealthListener
|
this.pickedSubchannelHealthListener
|
||||||
);
|
);
|
||||||
|
this.orcaClient?.close();
|
||||||
|
this.orcaClient = null;
|
||||||
|
this.metricsCall?.cancel();
|
||||||
|
this.metricsCall = null;
|
||||||
|
this.metricsBackoffTimer.stop();
|
||||||
|
this.metricsBackoffTimer.reset();
|
||||||
// Unref last, to avoid triggering listeners
|
// Unref last, to avoid triggering listeners
|
||||||
this.currentPick.unref();
|
this.currentPick.unref();
|
||||||
this.currentPick = null;
|
this.currentPick = null;
|
||||||
|
@ -439,6 +460,7 @@ export class PickFirstLoadBalancer implements LoadBalancer {
|
||||||
this.currentPick = subchannel;
|
this.currentPick = subchannel;
|
||||||
clearTimeout(this.connectionDelayTimeout);
|
clearTimeout(this.connectionDelayTimeout);
|
||||||
this.calculateAndReportNewState();
|
this.calculateAndReportNewState();
|
||||||
|
this.updateMetricsSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateState(newState: ConnectivityState, picker: Picker, errorMessage: string | null) {
|
private updateState(newState: ConnectivityState, picker: Picker, errorMessage: string | null) {
|
||||||
|
@ -573,6 +595,67 @@ export class PickFirstLoadBalancer implements LoadBalancer {
|
||||||
getTypeName(): string {
|
getTypeName(): string {
|
||||||
return TYPE_NAME;
|
return TYPE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getOrCreateOrcaClient(): OpenRcaServiceClient | null {
|
||||||
|
if (this.orcaClient) {
|
||||||
|
return this.orcaClient;
|
||||||
|
}
|
||||||
|
if (this.currentPick) {
|
||||||
|
const channel = this.currentPick.getChannel();
|
||||||
|
this.orcaClient = createOrcaClient(channel);
|
||||||
|
return this.orcaClient;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMetricsSubscription() {
|
||||||
|
if (this.orcaUnsupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.metricsListeners.size > 0) {
|
||||||
|
const newInterval = Math.min(...Array.from(this.metricsListeners.values()));
|
||||||
|
if (!this.metricsCall || newInterval !== this.currentMetricsIntervalMs) {
|
||||||
|
const orcaClient = this.getOrCreateOrcaClient();
|
||||||
|
if (!orcaClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.metricsCall?.cancel();
|
||||||
|
this.currentMetricsIntervalMs = newInterval;
|
||||||
|
const metricsCall = orcaClient.streamCoreMetrics({report_interval: msToDuration(newInterval)});
|
||||||
|
this.metricsCall = metricsCall;
|
||||||
|
metricsCall.on('data', (report: OrcaLoadReport__Output) => {
|
||||||
|
this.metricsListeners.forEach((interval, listener) => {
|
||||||
|
listener(report);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
metricsCall.on('error', (error: ServiceError) => {
|
||||||
|
this.metricsCall = null;
|
||||||
|
if (error.code === Status.UNIMPLEMENTED) {
|
||||||
|
this.orcaUnsupported = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error.code === Status.CANCELLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.metricsBackoffTimer.runOnce();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.metricsCall?.cancel();
|
||||||
|
this.metricsCall = null;
|
||||||
|
this.currentMetricsIntervalMs = Infinity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMetricsSubscription(listener: MetricsListener, intervalMs: number): void {
|
||||||
|
this.metricsListeners.set(listener, intervalMs);
|
||||||
|
this.updateMetricsSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMetricsSubscription(listener: MetricsListener): void {
|
||||||
|
this.metricsListeners.delete(listener);
|
||||||
|
this.updateMetricsSubscription();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEAF_CONFIG = new PickFirstLoadBalancingConfig(false);
|
const LEAF_CONFIG = new PickFirstLoadBalancingConfig(false);
|
||||||
|
@ -650,6 +733,14 @@ export class LeafLoadBalancer {
|
||||||
destroy() {
|
destroy() {
|
||||||
this.pickFirstBalancer.destroy();
|
this.pickFirstBalancer.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addMetricsSubscription(listener: MetricsListener, intervalMs: number): void {
|
||||||
|
this.pickFirstBalancer.addMetricsSubscription(listener, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMetricsSubscription(listener: MetricsListener): void {
|
||||||
|
this.pickFirstBalancer.removeMetricsSubscription(listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setup(): void {
|
export function setup(): void {
|
||||||
|
|
|
@ -20,9 +20,11 @@ import { OrcaLoadReport } from "./generated/xds/data/orca/v3/OrcaLoadReport";
|
||||||
import type { loadSync } from '@grpc/proto-loader';
|
import type { loadSync } from '@grpc/proto-loader';
|
||||||
import { ProtoGrpcType as OrcaProtoGrpcType } from "./generated/orca";
|
import { ProtoGrpcType as OrcaProtoGrpcType } from "./generated/orca";
|
||||||
import { loadPackageDefinition } from "./make-client";
|
import { loadPackageDefinition } from "./make-client";
|
||||||
import { OpenRcaServiceHandlers } from "./generated/xds/service/orca/v3/OpenRcaService";
|
import { OpenRcaServiceClient, OpenRcaServiceHandlers } from "./generated/xds/service/orca/v3/OpenRcaService";
|
||||||
import { durationMessageToDuration, durationToMs } from "./duration";
|
import { durationMessageToDuration, durationToMs } from "./duration";
|
||||||
import { Server } from "./server";
|
import { Server } from "./server";
|
||||||
|
import { ChannelCredentials } from "./channel-credentials";
|
||||||
|
import { Channel } from "./channel";
|
||||||
|
|
||||||
const loadedOrcaProto: OrcaProtoGrpcType | null = null;
|
const loadedOrcaProto: OrcaProtoGrpcType | null = null;
|
||||||
function loadOrcaProto(): OrcaProtoGrpcType {
|
function loadOrcaProto(): OrcaProtoGrpcType {
|
||||||
|
@ -206,3 +208,8 @@ export class ServerMetricRecorder {
|
||||||
server.addService(serviceDefinition, this.serviceImplementation);
|
server.addService(serviceDefinition, this.serviceImplementation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createOrcaClient(channel: Channel): OpenRcaServiceClient {
|
||||||
|
const ClientClass = loadOrcaProto().xds.service.orca.v3.OpenRcaService;
|
||||||
|
return new ClientClass('unused', ChannelCredentials.createInsecure(), {channelOverride: channel});
|
||||||
|
}
|
||||||
|
|
|
@ -24,12 +24,13 @@ import { ChannelOptions } from "./channel-options";
|
||||||
import { ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, unregisterChannelzRef } from "./channelz";
|
import { ChannelRef, ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerChannelzChannel, unregisterChannelzRef } from "./channelz";
|
||||||
import { ConnectivityState } from "./connectivity-state";
|
import { ConnectivityState } from "./connectivity-state";
|
||||||
import { Propagate, Status } from "./constants";
|
import { Propagate, Status } from "./constants";
|
||||||
|
import { restrictControlPlaneStatusCode } from "./control-plane-status";
|
||||||
import { Deadline, getRelativeTimeout } from "./deadline";
|
import { Deadline, getRelativeTimeout } from "./deadline";
|
||||||
import { Metadata } from "./metadata";
|
import { Metadata } from "./metadata";
|
||||||
import { getDefaultAuthority } from "./resolver";
|
import { getDefaultAuthority } from "./resolver";
|
||||||
import { Subchannel } from "./subchannel";
|
import { Subchannel } from "./subchannel";
|
||||||
import { SubchannelCall } from "./subchannel-call";
|
import { SubchannelCall } from "./subchannel-call";
|
||||||
import { GrpcUri, uriToString } from "./uri-parser";
|
import { GrpcUri, splitHostPort, uriToString } from "./uri-parser";
|
||||||
|
|
||||||
class SubchannelCallWrapper implements Call {
|
class SubchannelCallWrapper implements Call {
|
||||||
private childCall: SubchannelCall | null = null;
|
private childCall: SubchannelCall | null = null;
|
||||||
|
@ -38,7 +39,20 @@ class SubchannelCallWrapper implements Call {
|
||||||
private readPending = false;
|
private readPending = false;
|
||||||
private halfClosePending = false;
|
private halfClosePending = false;
|
||||||
private pendingStatus: StatusObject | null = null;
|
private pendingStatus: StatusObject | null = null;
|
||||||
|
private serviceUrl: string;
|
||||||
constructor(private subchannel: Subchannel, private method: string, private options: CallStreamOptions, private callNumber: number) {
|
constructor(private subchannel: Subchannel, private method: string, private options: CallStreamOptions, private callNumber: number) {
|
||||||
|
const splitPath: string[] = this.method.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(this.options.host)?.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}`;
|
||||||
const timeout = getRelativeTimeout(options.deadline);
|
const timeout = getRelativeTimeout(options.deadline);
|
||||||
if (timeout !== Infinity) {
|
if (timeout !== Infinity) {
|
||||||
if (timeout <= 0) {
|
if (timeout <= 0) {
|
||||||
|
@ -79,16 +93,32 @@ class SubchannelCallWrapper implements Call {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.childCall = this.subchannel.createCall(metadata, this.options.host, this.method, listener);
|
this.subchannel.getCallCredentials()
|
||||||
if (this.readPending) {
|
.generateMetadata({method_name: this.method, service_url: this.serviceUrl})
|
||||||
this.childCall.startRead();
|
.then(credsMetadata => {
|
||||||
}
|
this.childCall = this.subchannel.createCall(credsMetadata, this.options.host, this.method, listener);
|
||||||
if (this.pendingMessage) {
|
if (this.readPending) {
|
||||||
this.childCall.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message);
|
this.childCall.startRead();
|
||||||
}
|
}
|
||||||
if (this.halfClosePending) {
|
if (this.pendingMessage) {
|
||||||
this.childCall.halfClose();
|
this.childCall.sendMessageWithContext(this.pendingMessage.context, this.pendingMessage.message);
|
||||||
}
|
}
|
||||||
|
if (this.halfClosePending) {
|
||||||
|
this.childCall.halfClose();
|
||||||
|
}
|
||||||
|
}, (error: Error & { code: number }) => {
|
||||||
|
const { code, details } = restrictControlPlaneStatusCode(
|
||||||
|
typeof error.code === 'number' ? error.code : Status.UNKNOWN,
|
||||||
|
`Getting metadata from plugin failed with error: ${error.message}`
|
||||||
|
);
|
||||||
|
listener.onReceiveStatus(
|
||||||
|
{
|
||||||
|
code: code,
|
||||||
|
details: details,
|
||||||
|
metadata: new Metadata(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
sendMessageWithContext(context: MessageContext, message: Buffer): void {
|
sendMessageWithContext(context: MessageContext, message: Buffer): void {
|
||||||
if (this.childCall) {
|
if (this.childCall) {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CallCredentials } from './call-credentials';
|
import { CallCredentials } from './call-credentials';
|
||||||
|
import { Channel } from './channel';
|
||||||
import type { SubchannelRef } from './channelz';
|
import type { SubchannelRef } from './channelz';
|
||||||
import { ConnectivityState } from './connectivity-state';
|
import { ConnectivityState } from './connectivity-state';
|
||||||
import { Subchannel } from './subchannel';
|
import { Subchannel } from './subchannel';
|
||||||
|
@ -67,6 +68,10 @@ export interface SubchannelInterface {
|
||||||
* subchannel.
|
* subchannel.
|
||||||
*/
|
*/
|
||||||
getCallCredentials(): CallCredentials;
|
getCallCredentials(): CallCredentials;
|
||||||
|
/**
|
||||||
|
* Get a channel that can be used to make requests with just this
|
||||||
|
*/
|
||||||
|
getChannel(): Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseSubchannelWrapper implements SubchannelInterface {
|
export abstract class BaseSubchannelWrapper implements SubchannelInterface {
|
||||||
|
@ -143,4 +148,7 @@ export abstract class BaseSubchannelWrapper implements SubchannelInterface {
|
||||||
getCallCredentials(): CallCredentials {
|
getCallCredentials(): CallCredentials {
|
||||||
return this.child.getCallCredentials();
|
return this.child.getCallCredentials();
|
||||||
}
|
}
|
||||||
|
getChannel(): Channel {
|
||||||
|
return this.child.getChannel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,8 @@ import { SubchannelCallInterceptingListener } from './subchannel-call';
|
||||||
import { SubchannelCall } from './subchannel-call';
|
import { SubchannelCall } from './subchannel-call';
|
||||||
import { CallEventTracker, SubchannelConnector, Transport } from './transport';
|
import { CallEventTracker, SubchannelConnector, Transport } from './transport';
|
||||||
import { CallCredentials } from './call-credentials';
|
import { CallCredentials } from './call-credentials';
|
||||||
|
import { SingleSubchannelChannel } from './single-subchannel-channel';
|
||||||
|
import { Channel } from './channel';
|
||||||
|
|
||||||
const TRACER_NAME = 'subchannel';
|
const TRACER_NAME = 'subchannel';
|
||||||
|
|
||||||
|
@ -519,4 +521,8 @@ export class Subchannel implements SubchannelInterface {
|
||||||
getCallCredentials(): CallCredentials {
|
getCallCredentials(): CallCredentials {
|
||||||
return this.secureConnector.getCallCredentials();
|
return this.secureConnector.getCallCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChannel(): Channel {
|
||||||
|
return new SingleSubchannelChannel(this, this.channelTarget, this.options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue