mirror of https://github.com/grpc/grpc-node.git
grpc-js-xds: Refactor matcher and routeAction for logging, add more interop client logging
This commit is contained in:
parent
d160a2f248
commit
b995fa62cf
|
@ -303,6 +303,8 @@ function main() {
|
|||
currentConfig.callTypes = call.request.types;
|
||||
currentConfig.metadata = callMetadata;
|
||||
currentConfig.timeoutSec = call.request.timeout_sec
|
||||
console.log('Received new client configuration: ' + JSON.stringify(currentConfig, undefined, 2));
|
||||
callback(null, {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* 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 { Metadata } from "@grpc/grpc-js";
|
||||
import { RE2 } from "re2-wasm";
|
||||
|
||||
/**
|
||||
* An object representing a predicate that determines whether a given
|
||||
* combination of a methodName and metadata matches some internal conditions.
|
||||
*/
|
||||
export interface Matcher {
|
||||
toString(): string;
|
||||
apply(methodName: string, metadata: Metadata): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing a predicate that determines whether a given string
|
||||
* value matches some internal conditions.
|
||||
*/
|
||||
export interface ValueMatcher {
|
||||
toString(): string;
|
||||
apply(value: string): boolean;
|
||||
}
|
||||
|
||||
export class ExactValueMatcher implements ValueMatcher {
|
||||
constructor(private targetValue: string) {}
|
||||
|
||||
apply(value: string) {
|
||||
return value === this.targetValue;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Exact(' + this.targetValue + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeRegexValueMatcher implements ValueMatcher {
|
||||
private targetRegexImpl: RE2;
|
||||
constructor(targetRegex: string) {
|
||||
this.targetRegexImpl = new RE2(`^${targetRegex}$`, 'u');
|
||||
}
|
||||
|
||||
apply(value: string) {
|
||||
return this.targetRegexImpl.test(value);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'SafeRegex(' + this.targetRegexImpl.toString() + ')';
|
||||
}
|
||||
}
|
||||
|
||||
const numberRegex = new RE2(/^-?\d+$/u);
|
||||
|
||||
export class RangeValueMatcher implements ValueMatcher {
|
||||
constructor(private start: BigInt, private end: BigInt) {}
|
||||
|
||||
apply(value: string) {
|
||||
if (!numberRegex.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const numberValue = BigInt(value);
|
||||
return this.start <= numberValue && numberValue < this.end;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Range(' + this.start + ', ' + this.end + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class PresentValueMatcher implements ValueMatcher {
|
||||
constructor() {}
|
||||
|
||||
apply(value: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Present()';
|
||||
}
|
||||
}
|
||||
|
||||
export class PrefixValueMatcher implements ValueMatcher {
|
||||
constructor(private prefix: string) {}
|
||||
|
||||
apply(value: string) {
|
||||
return value.startsWith(this.prefix);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Prefix(' + this.prefix + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class SuffixValueMatcher implements ValueMatcher {
|
||||
constructor(private suffix: string) {}
|
||||
|
||||
apply(value: string) {
|
||||
return value.endsWith(this.suffix);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Suffix(' + this.suffix + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class RejectValueMatcher implements ValueMatcher {
|
||||
constructor() {}
|
||||
|
||||
apply(value: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Reject()';
|
||||
}
|
||||
}
|
||||
|
||||
export class HeaderMatcher implements Matcher {
|
||||
constructor(private headerName: string, private valueMatcher: ValueMatcher, private invertMatch: boolean) {}
|
||||
|
||||
private applyHelper(methodName: string, metadata: Metadata) {
|
||||
if (this.headerName.endsWith('-bin')) {
|
||||
return false;
|
||||
}
|
||||
let value: string;
|
||||
if (this.headerName === 'content-type') {
|
||||
value = 'application/grpc';
|
||||
} else {
|
||||
const valueArray = metadata.get(this.headerName);
|
||||
if (valueArray.length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
value = valueArray.join(',');
|
||||
}
|
||||
}
|
||||
return this.valueMatcher.apply(value);
|
||||
}
|
||||
|
||||
apply(methodName: string, metadata: Metadata) {
|
||||
const result = this.applyHelper(methodName, metadata);
|
||||
if (this.invertMatch) {
|
||||
return !result;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'HeaderMatch(' + this.headerName + ', ' + this.valueMatcher.toString() + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class PathPrefixValueMatcher {
|
||||
constructor(private prefix: string, private caseInsensitive: boolean) {}
|
||||
|
||||
apply(value: string) {
|
||||
if (this.caseInsensitive) {
|
||||
return value.toLowerCase().startsWith(this.prefix.toLowerCase());
|
||||
} else {
|
||||
return value.startsWith(this.prefix);
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Prefix(' + this.prefix + ', ' + this.caseInsensitive + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class PathExactValueMatcher {
|
||||
constructor(private targetValue: string, private caseInsensitive: boolean) {}
|
||||
|
||||
apply(value: string) {
|
||||
if (this.caseInsensitive) {
|
||||
return value.toLowerCase().startsWith(this.targetValue.toLowerCase());
|
||||
} else {
|
||||
return value === this.targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'Exact(' + this.targetValue + ', ' + this.caseInsensitive + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export class PathSafeRegexValueMatcher {
|
||||
private targetRegexImpl: RE2;
|
||||
constructor(targetRegex: string, caseInsensitive: boolean) {
|
||||
this.targetRegexImpl = new RE2(`^${targetRegex}$`, caseInsensitive ? 'iu' : 'u');
|
||||
}
|
||||
|
||||
apply(value: string) {
|
||||
return this.targetRegexImpl.test(value);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'SafeRegex(' + this.targetRegexImpl.toString() + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export interface Fraction {
|
||||
numerator: number;
|
||||
denominator: number;
|
||||
}
|
||||
|
||||
function fractionToString(fraction: Fraction): string {
|
||||
return `${fraction.numerator}/${fraction.denominator}`;
|
||||
}
|
||||
|
||||
export class FullMatcher implements Matcher {
|
||||
constructor(private pathMatcher: ValueMatcher, private headerMatchers: Matcher[], private fraction: Fraction | null) {}
|
||||
|
||||
apply(methodName: string, metadata: Metadata) {
|
||||
if (!this.pathMatcher.apply(methodName)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.headerMatchers.every(matcher => matcher.apply(methodName, metadata))) {
|
||||
return false;
|
||||
}
|
||||
if (this.fraction === null) {
|
||||
return true;
|
||||
} else {
|
||||
const randomNumber = Math.random() * this.fraction.denominator;
|
||||
return randomNumber < this.fraction.numerator;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `path: ${this.pathMatcher}
|
||||
headers: ${this.headerMatchers.map(matcher => matcher.toString()).join('\n\t')}
|
||||
fraction: ${this.fraction ? fractionToString(this.fraction): 'none'}`;
|
||||
}
|
||||
}
|
|
@ -38,6 +38,8 @@ import { HeaderMatcher__Output } from './generated/envoy/api/v2/route/HeaderMatc
|
|||
import ConfigSelector = experimental.ConfigSelector;
|
||||
import LoadBalancingConfig = experimental.LoadBalancingConfig;
|
||||
import { XdsClusterManagerLoadBalancingConfig } from './load-balancer-xds-cluster-manager';
|
||||
import { ExactValueMatcher, Fraction, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher';
|
||||
import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action';
|
||||
|
||||
const TRACER_NAME = 'xds_resolver';
|
||||
|
||||
|
@ -119,68 +121,35 @@ function findVirtualHostForDomain(virutalHostList: VirtualHost__Output[], domain
|
|||
return targetVhost;
|
||||
}
|
||||
|
||||
interface Matcher {
|
||||
(methodName: string, metadata: Metadata): boolean;
|
||||
}
|
||||
|
||||
const numberRegex = new RE2(/^-?\d+$/u);
|
||||
|
||||
function getPredicateForHeaderMatcher(headerMatch: HeaderMatcher__Output): Matcher {
|
||||
let valueChecker: (value: string) => boolean;
|
||||
let valueChecker: ValueMatcher;
|
||||
switch (headerMatch.header_match_specifier) {
|
||||
case 'exact_match':
|
||||
valueChecker = value => value === headerMatch.exact_match;
|
||||
valueChecker = new ExactValueMatcher(headerMatch.exact_match!);
|
||||
break;
|
||||
case 'safe_regex_match':
|
||||
const regex = new RE2(`^${headerMatch.safe_regex_match}$`, 'u');
|
||||
valueChecker = value => regex.test(value);
|
||||
valueChecker = new SafeRegexValueMatcher(headerMatch.safe_regex_match!.regex);
|
||||
break;
|
||||
case 'range_match':
|
||||
const start = BigInt(headerMatch.range_match!.start);
|
||||
const end = BigInt(headerMatch.range_match!.end);
|
||||
valueChecker = value => {
|
||||
if (!numberRegex.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const numberValue = BigInt(value);
|
||||
return start <= numberValue && numberValue < end;
|
||||
}
|
||||
valueChecker = new RangeValueMatcher(start, end);
|
||||
break;
|
||||
case 'present_match':
|
||||
valueChecker = value => true;
|
||||
valueChecker = new PresentValueMatcher();
|
||||
break;
|
||||
case 'prefix_match':
|
||||
valueChecker = value => value.startsWith(headerMatch.prefix_match!);
|
||||
valueChecker = new PrefixValueMatcher(headerMatch.prefix_match!);
|
||||
break;
|
||||
case 'suffix_match':
|
||||
valueChecker = value => value.endsWith(headerMatch.suffix_match!);
|
||||
valueChecker = new SuffixValueMatcher(headerMatch.suffix_match!);
|
||||
break;
|
||||
default:
|
||||
// Should be prevented by validation rules
|
||||
return (methodName, metadata) => false;
|
||||
}
|
||||
const headerMatcher: Matcher = (methodName, metadata) => {
|
||||
if (headerMatch.name.endsWith('-bin')) {
|
||||
return false;
|
||||
}
|
||||
let value: string;
|
||||
if (headerMatch.name === 'content-type') {
|
||||
value = 'application/grpc';
|
||||
} else {
|
||||
const valueArray = metadata.get(headerMatch.name);
|
||||
if (valueArray.length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
value = valueArray.join(',');
|
||||
}
|
||||
}
|
||||
return valueChecker(value);
|
||||
}
|
||||
if (headerMatch.invert_match) {
|
||||
return (methodName, metadata) => !headerMatcher(methodName, metadata);
|
||||
} else {
|
||||
return headerMatcher;
|
||||
valueChecker = new RejectValueMatcher();
|
||||
}
|
||||
return new HeaderMatcher(headerMatch.name, valueChecker, headerMatch.invert_match);
|
||||
}
|
||||
|
||||
const RUNTIME_FRACTION_DENOMINATOR_VALUES = {
|
||||
|
@ -190,48 +159,32 @@ const RUNTIME_FRACTION_DENOMINATOR_VALUES = {
|
|||
}
|
||||
|
||||
function getPredicateForMatcher(routeMatch: RouteMatch__Output): Matcher {
|
||||
let pathMatcher: Matcher;
|
||||
let pathMatcher: ValueMatcher;
|
||||
const caseInsensitive = routeMatch.case_sensitive?.value === false;
|
||||
switch (routeMatch.path_specifier) {
|
||||
case 'prefix':
|
||||
if (routeMatch.case_sensitive?.value === false) {
|
||||
const prefix = routeMatch.prefix!.toLowerCase();
|
||||
pathMatcher = (methodName, metadata) => (methodName.toLowerCase().startsWith(prefix));
|
||||
} else {
|
||||
const prefix = routeMatch.prefix!;
|
||||
pathMatcher = (methodName, metadata) => (methodName.startsWith(prefix));
|
||||
}
|
||||
pathMatcher = new PathPrefixValueMatcher(routeMatch.prefix!, caseInsensitive);
|
||||
break;
|
||||
case 'path':
|
||||
if (routeMatch.case_sensitive?.value === false) {
|
||||
const path = routeMatch.path!.toLowerCase();
|
||||
pathMatcher = (methodName, metadata) => (methodName.toLowerCase() === path);
|
||||
} else {
|
||||
const path = routeMatch.path!;
|
||||
pathMatcher = (methodName, metadata) => (methodName === path);
|
||||
}
|
||||
pathMatcher = new PathExactValueMatcher(routeMatch.path!, caseInsensitive);
|
||||
break;
|
||||
case 'safe_regex':
|
||||
const flags = routeMatch.case_sensitive?.value === false ? 'ui' : 'u';
|
||||
const regex = new RE2(`^${routeMatch.safe_regex!.regex!}$`, flags);
|
||||
pathMatcher = (methodName, metadata) => (regex.test(methodName));
|
||||
pathMatcher = new PathSafeRegexValueMatcher(routeMatch.safe_regex!.regex, caseInsensitive);
|
||||
break;
|
||||
default:
|
||||
// Should be prevented by validation rules
|
||||
return (methodName, metadata) => false;
|
||||
pathMatcher = new RejectValueMatcher();
|
||||
}
|
||||
const headerMatchers: Matcher[] = routeMatch.headers.map(getPredicateForHeaderMatcher);
|
||||
let runtimeFractionHandler: () => boolean;
|
||||
let runtimeFraction: Fraction | null;
|
||||
if (!routeMatch.runtime_fraction?.default_value) {
|
||||
runtimeFractionHandler = () => true;
|
||||
runtimeFraction = null;
|
||||
} else {
|
||||
const numerator = routeMatch.runtime_fraction.default_value.numerator;
|
||||
const denominator = RUNTIME_FRACTION_DENOMINATOR_VALUES[routeMatch.runtime_fraction.default_value.denominator];
|
||||
runtimeFractionHandler = () => {
|
||||
const randomNumber = Math.random() * denominator;
|
||||
return randomNumber < numerator;
|
||||
}
|
||||
runtimeFraction = {
|
||||
numerator: routeMatch.runtime_fraction.default_value.numerator,
|
||||
denominator: RUNTIME_FRACTION_DENOMINATOR_VALUES[routeMatch.runtime_fraction.default_value.denominator]
|
||||
};
|
||||
}
|
||||
return (methodName, metadata) => pathMatcher(methodName, metadata) && headerMatchers.every(matcher => matcher(methodName, metadata)) && runtimeFractionHandler();
|
||||
return new FullMatcher(pathMatcher, headerMatchers, runtimeFraction);
|
||||
}
|
||||
|
||||
class XdsResolver implements Resolver {
|
||||
|
@ -340,38 +293,27 @@ class XdsResolver implements Resolver {
|
|||
this.reportResolutionError('No matching route found');
|
||||
return;
|
||||
}
|
||||
trace('Received virtual host config ' + JSON.stringify(virtualHost, undefined, 2));
|
||||
const allConfigClusters = new Set<string>();
|
||||
const matchList: {matcher: Matcher, action: () => string}[] = [];
|
||||
const matchList: {matcher: Matcher, action: RouteAction}[] = [];
|
||||
for (const route of virtualHost.routes) {
|
||||
let routeAction: () => string;
|
||||
let routeAction: RouteAction;
|
||||
switch (route.route!.cluster_specifier) {
|
||||
case 'cluster_header':
|
||||
continue;
|
||||
case 'cluster':{
|
||||
const cluster = route.route!.cluster!;
|
||||
allConfigClusters.add(cluster);
|
||||
routeAction = () => cluster;
|
||||
routeAction = new SingleClusterRouteAction(cluster);
|
||||
break;
|
||||
}
|
||||
case 'weighted_clusters': {
|
||||
let lastNumerator = 0;
|
||||
// clusterChoices is essentially the weighted choices represented as a CDF
|
||||
const clusterChoices: {cluster: string, numerator: number}[] = [];
|
||||
const weightedClusters: WeightedCluster[] = [];
|
||||
for (const clusterWeight of route.route!.weighted_clusters!.clusters) {
|
||||
allConfigClusters.add(clusterWeight.name);
|
||||
lastNumerator = lastNumerator + (clusterWeight.weight?.value ?? 0);
|
||||
clusterChoices.push({cluster: clusterWeight.name, numerator: lastNumerator});
|
||||
}
|
||||
routeAction = () => {
|
||||
const randomNumber = Math.random() * (route.route!.weighted_clusters!.total_weight?.value ?? 100);
|
||||
for (const choice of clusterChoices) {
|
||||
if (randomNumber < choice.numerator) {
|
||||
return choice.cluster;
|
||||
}
|
||||
}
|
||||
// This should be prevented by the validation rules
|
||||
return '';
|
||||
weightedClusters.push({name: clusterWeight.name, weight: clusterWeight.weight?.value ?? 0});
|
||||
}
|
||||
routeAction = new WeightedClusterRouteAction(weightedClusters, route.route!.weighted_clusters!.total_weight?.value ?? 100);
|
||||
}
|
||||
}
|
||||
const routeMatcher = getPredicateForMatcher(route.match!);
|
||||
|
@ -397,8 +339,8 @@ class XdsResolver implements Resolver {
|
|||
}
|
||||
const configSelector: ConfigSelector = (methodName, metadata) => {
|
||||
for (const {matcher, action} of matchList) {
|
||||
if (matcher(methodName, metadata)) {
|
||||
const clusterName = action();
|
||||
if (matcher.apply(methodName, metadata)) {
|
||||
const clusterName = action.getCluster();
|
||||
this.refCluster(clusterName);
|
||||
const onCommitted = () => {
|
||||
this.unrefCluster(clusterName);
|
||||
|
@ -418,6 +360,11 @@ class XdsResolver implements Resolver {
|
|||
status: status.UNAVAILABLE
|
||||
};
|
||||
};
|
||||
trace('Created ConfigSelector with configuration:');
|
||||
for (const {matcher, action} of matchList) {
|
||||
trace(matcher.toString());
|
||||
trace('=> ' + action.toString());
|
||||
}
|
||||
const clusterConfigMap = new Map<string, {child_policy: LoadBalancingConfig[]}>();
|
||||
for (const clusterName of this.clusterRefcounts.keys()) {
|
||||
clusterConfigMap.set(clusterName, {child_policy: [new CdsLoadBalancingConfig(clusterName)]});
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 interface RouteAction {
|
||||
toString(): string;
|
||||
getCluster(): string;
|
||||
}
|
||||
|
||||
export class SingleClusterRouteAction implements RouteAction {
|
||||
constructor(private cluster: string) {}
|
||||
|
||||
getCluster() {
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'SingleCluster(' + this.cluster + ')';
|
||||
}
|
||||
}
|
||||
|
||||
export interface WeightedCluster {
|
||||
name: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
interface ClusterChoice {
|
||||
name: string;
|
||||
numerator: number;
|
||||
}
|
||||
|
||||
export class WeightedClusterRouteAction implements RouteAction {
|
||||
/**
|
||||
* The weighted cluster choices represented as a CDF
|
||||
*/
|
||||
private clusterChoices: ClusterChoice[];
|
||||
constructor(private clusters: WeightedCluster[], private totalWeight: number) {
|
||||
this.clusterChoices = [];
|
||||
let lastNumerator = 0;
|
||||
for (const clusterWeight of clusters) {
|
||||
lastNumerator += clusterWeight.weight;
|
||||
this.clusterChoices.push({name: clusterWeight.name, numerator: lastNumerator});
|
||||
}
|
||||
}
|
||||
|
||||
getCluster() {
|
||||
const randomNumber = Math.random() * this.totalWeight;
|
||||
for (const choice of this.clusterChoices) {
|
||||
if (randomNumber < choice.numerator) {
|
||||
return choice.name;
|
||||
}
|
||||
}
|
||||
// This should be prevented by the validation rules
|
||||
return '';
|
||||
}
|
||||
|
||||
toString() {
|
||||
return 'WeightedCluster(' + this.clusters.map(({name, weight}) => '(' + name + ':' + weight + ')').join(', ') + ')';
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue