xdsrouting: all matchers (#3733)
This commit is contained in:
20
xds/internal/balancer/xdsrouting/doc.go
Normal file
20
xds/internal/balancer/xdsrouting/doc.go
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package xdsrouting implements the routing balancer for xds.
|
||||
package xdsrouting
|
134
xds/internal/balancer/xdsrouting/matcher.go
Normal file
134
xds/internal/balancer/xdsrouting/matcher.go
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsrouting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/internal/grpcrand"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// compositeMatcher.match returns true if all matchers return true.
|
||||
type compositeMatcher struct {
|
||||
pm pathMatcherInterface
|
||||
hms []headerMatcherInterface
|
||||
fm *fractionMatcher
|
||||
}
|
||||
|
||||
func newCompositeMatcher(pm pathMatcherInterface, hms []headerMatcherInterface, fm *fractionMatcher) *compositeMatcher {
|
||||
return &compositeMatcher{pm: pm, hms: hms, fm: fm}
|
||||
}
|
||||
|
||||
func (a *compositeMatcher) match(info balancer.PickInfo) bool {
|
||||
if a.pm != nil && !a.pm.match(info.FullMethodName) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Call headerMatchers even if md is nil, because routes may match
|
||||
// non-presence of some headers.
|
||||
var md metadata.MD
|
||||
if info.Ctx != nil {
|
||||
md, _ = metadata.FromOutgoingContext(info.Ctx)
|
||||
}
|
||||
for _, m := range a.hms {
|
||||
if !m.match(md) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if a.fm != nil && !a.fm.match() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *compositeMatcher) equal(mm *compositeMatcher) bool {
|
||||
if a == mm {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil || mm == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if (a.pm != nil || mm.pm != nil) && (a.pm == nil || !a.pm.equal(mm.pm)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a.hms) != len(mm.hms) {
|
||||
return false
|
||||
}
|
||||
for i := range a.hms {
|
||||
if !a.hms[i].equal(mm.hms[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (a.fm != nil || mm.fm != nil) && (a.fm == nil || !a.fm.equal(mm.fm)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *compositeMatcher) String() string {
|
||||
var ret string
|
||||
if a.pm != nil {
|
||||
ret += a.pm.String()
|
||||
}
|
||||
for _, m := range a.hms {
|
||||
ret += m.String()
|
||||
}
|
||||
if a.fm != nil {
|
||||
ret += a.fm.String()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type fractionMatcher struct {
|
||||
fraction int64 // real fraction is fraction/1,000,000.
|
||||
}
|
||||
|
||||
func newFractionMatcher(fraction uint32) *fractionMatcher {
|
||||
return &fractionMatcher{fraction: int64(fraction)}
|
||||
}
|
||||
|
||||
var grpcrandInt63n = grpcrand.Int63n
|
||||
|
||||
func (fm *fractionMatcher) match() bool {
|
||||
t := grpcrandInt63n(1000000)
|
||||
return t <= fm.fraction
|
||||
}
|
||||
|
||||
func (fm *fractionMatcher) equal(m *fractionMatcher) bool {
|
||||
if fm == m {
|
||||
return true
|
||||
}
|
||||
if fm == nil || m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fm.fraction == m.fraction
|
||||
}
|
||||
|
||||
func (fm *fractionMatcher) String() string {
|
||||
return fmt.Sprintf("fraction:%v", fm.fraction)
|
||||
}
|
245
xds/internal/balancer/xdsrouting/matcher_header.go
Normal file
245
xds/internal/balancer/xdsrouting/matcher_header.go
Normal file
@ -0,0 +1,245 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsrouting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type headerMatcherInterface interface {
|
||||
match(metadata.MD) bool
|
||||
equal(headerMatcherInterface) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
// mdValuesFromOutgoingCtx retrieves metadata from context. If there are
|
||||
// multiple values, the values are concatenated with "," (comma and no space).
|
||||
//
|
||||
// All header matchers only match against the comma-concatenated string.
|
||||
func mdValuesFromOutgoingCtx(md metadata.MD, key string) (string, bool) {
|
||||
vs, ok := md[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return strings.Join(vs, ","), true
|
||||
}
|
||||
|
||||
type headerExactMatcher struct {
|
||||
key string
|
||||
exact string
|
||||
}
|
||||
|
||||
func newHeaderExactMatcher(key, exact string) *headerExactMatcher {
|
||||
return &headerExactMatcher{key: key, exact: exact}
|
||||
}
|
||||
|
||||
func (hem *headerExactMatcher) match(md metadata.MD) bool {
|
||||
v, ok := mdValuesFromOutgoingCtx(md, hem.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return v == hem.exact
|
||||
}
|
||||
|
||||
func (hem *headerExactMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*headerExactMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hem.key == mm.key && hem.exact == mm.exact
|
||||
}
|
||||
|
||||
func (hem *headerExactMatcher) String() string {
|
||||
return fmt.Sprintf("headerExact:%v:%v", hem.key, hem.exact)
|
||||
}
|
||||
|
||||
type headerRegexMatcher struct {
|
||||
key string
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func newHeaderRegexMatcher(key string, re *regexp.Regexp) *headerRegexMatcher {
|
||||
return &headerRegexMatcher{key: key, re: re}
|
||||
}
|
||||
|
||||
func (hrm *headerRegexMatcher) match(md metadata.MD) bool {
|
||||
v, ok := mdValuesFromOutgoingCtx(md, hrm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hrm.re.MatchString(v)
|
||||
}
|
||||
|
||||
func (hrm *headerRegexMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*headerRegexMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hrm.key == mm.key && hrm.re.String() == mm.re.String()
|
||||
}
|
||||
|
||||
func (hrm *headerRegexMatcher) String() string {
|
||||
return fmt.Sprintf("headerRegex:%v:%v", hrm.key, hrm.re.String())
|
||||
}
|
||||
|
||||
type headerRangeMatcher struct {
|
||||
key string
|
||||
start, end int64 // represents [start, end).
|
||||
}
|
||||
|
||||
func newHeaderRangeMatcher(key string, start, end int64) *headerRangeMatcher {
|
||||
return &headerRangeMatcher{key: key, start: start, end: end}
|
||||
}
|
||||
|
||||
func (hrm *headerRangeMatcher) match(md metadata.MD) bool {
|
||||
v, ok := mdValuesFromOutgoingCtx(md, hrm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil && i >= hrm.start && i < hrm.end {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (hrm *headerRangeMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*headerRangeMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hrm.key == mm.key && hrm.start == mm.start && hrm.end == mm.end
|
||||
}
|
||||
|
||||
func (hrm *headerRangeMatcher) String() string {
|
||||
return fmt.Sprintf("headerRange:%v:[%d,%d)", hrm.key, hrm.start, hrm.end)
|
||||
}
|
||||
|
||||
type headerPresentMatcher struct {
|
||||
key string
|
||||
present bool
|
||||
}
|
||||
|
||||
func newHeaderPresentMatcher(key string, present bool) *headerPresentMatcher {
|
||||
return &headerPresentMatcher{key: key, present: present}
|
||||
}
|
||||
|
||||
func (hpm *headerPresentMatcher) match(md metadata.MD) bool {
|
||||
vs, ok := mdValuesFromOutgoingCtx(md, hpm.key)
|
||||
present := ok && len(vs) > 0
|
||||
return present == hpm.present
|
||||
}
|
||||
|
||||
func (hpm *headerPresentMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*headerPresentMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hpm.key == mm.key && hpm.present == mm.present
|
||||
}
|
||||
|
||||
func (hpm *headerPresentMatcher) String() string {
|
||||
return fmt.Sprintf("headerPresent:%v:%v", hpm.key, hpm.present)
|
||||
}
|
||||
|
||||
type headerPrefixMatcher struct {
|
||||
key string
|
||||
prefix string
|
||||
}
|
||||
|
||||
func newHeaderPrefixMatcher(key string, prefix string) *headerPrefixMatcher {
|
||||
return &headerPrefixMatcher{key: key, prefix: prefix}
|
||||
}
|
||||
|
||||
func (hpm *headerPrefixMatcher) match(md metadata.MD) bool {
|
||||
v, ok := mdValuesFromOutgoingCtx(md, hpm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(v, hpm.prefix)
|
||||
}
|
||||
|
||||
func (hpm *headerPrefixMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*headerPrefixMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hpm.key == mm.key && hpm.prefix == mm.prefix
|
||||
}
|
||||
|
||||
func (hpm *headerPrefixMatcher) String() string {
|
||||
return fmt.Sprintf("headerPrefix:%v:%v", hpm.key, hpm.prefix)
|
||||
}
|
||||
|
||||
type headerSuffixMatcher struct {
|
||||
key string
|
||||
suffix string
|
||||
}
|
||||
|
||||
func newHeaderSuffixMatcher(key string, suffix string) *headerSuffixMatcher {
|
||||
return &headerSuffixMatcher{key: key, suffix: suffix}
|
||||
}
|
||||
|
||||
func (hsm *headerSuffixMatcher) match(md metadata.MD) bool {
|
||||
v, ok := mdValuesFromOutgoingCtx(md, hsm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(v, hsm.suffix)
|
||||
}
|
||||
|
||||
func (hsm *headerSuffixMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*headerSuffixMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hsm.key == mm.key && hsm.suffix == mm.suffix
|
||||
}
|
||||
|
||||
func (hsm *headerSuffixMatcher) String() string {
|
||||
return fmt.Sprintf("headerSuffix:%v:%v", hsm.key, hsm.suffix)
|
||||
}
|
||||
|
||||
type invertMatcher struct {
|
||||
m headerMatcherInterface
|
||||
}
|
||||
|
||||
func newInvertMatcher(m headerMatcherInterface) *invertMatcher {
|
||||
return &invertMatcher{m: m}
|
||||
}
|
||||
|
||||
func (i *invertMatcher) match(md metadata.MD) bool {
|
||||
return !i.m.match(md)
|
||||
}
|
||||
|
||||
func (i *invertMatcher) equal(m headerMatcherInterface) bool {
|
||||
mm, ok := m.(*invertMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return i.m.equal(mm.m)
|
||||
}
|
||||
|
||||
func (i *invertMatcher) String() string {
|
||||
return fmt.Sprintf("invert{%s}", i.m)
|
||||
}
|
333
xds/internal/balancer/xdsrouting/matcher_header_test.go
Normal file
333
xds/internal/balancer/xdsrouting/matcher_header_test.go
Normal file
@ -0,0 +1,333 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsrouting
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestHeaderExactMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, exact string
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "tv"),
|
||||
// Doesn't match comma-concatenated string.
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
exact: "abc,tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hem := newHeaderExactMatcher(tt.key, tt.exact)
|
||||
if got := hem.match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderRegexMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, regexStr string
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "tttvv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "abc", "th", "tttvv"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
regexStr: "^[abc]*,t+v*$",
|
||||
md: metadata.Pairs("th", "abc", "th", "tttvv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hrm := newHeaderRegexMatcher(tt.key, regexp.MustCompile(tt.regexStr))
|
||||
if got := hrm.match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderRangeMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
start, end int64
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "match",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "5"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal to start",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "1"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal to end",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "10"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "negative",
|
||||
key: "th",
|
||||
start: -10, end: 10,
|
||||
md: metadata.Pairs("th", "-5"),
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hrm := newHeaderRangeMatcher(tt.key, tt.start, tt.end)
|
||||
if got := hrm.match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderPresentMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
present bool
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "want present is present",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "want present not present",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs("abc", "tv"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "want not present is present",
|
||||
key: "th",
|
||||
present: false,
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "want not present is not present",
|
||||
key: "th",
|
||||
present: false,
|
||||
md: metadata.Pairs("abc", "tv"),
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hpm := newHeaderPresentMatcher(tt.key, tt.present)
|
||||
if got := hpm.match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderPrefixMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, prefix string
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "tv123"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "tv123"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "tv123", "th", "abc"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hpm := newHeaderPrefixMatcher(tt.key, tt.prefix)
|
||||
if got := hpm.match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderSuffixMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, suffix string
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "123tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "123tv", "th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "123tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hsm := newHeaderSuffixMatcher(tt.key, tt.suffix)
|
||||
if got := hsm.match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvertMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
m headerMatcherInterface
|
||||
md metadata.MD
|
||||
}{
|
||||
{
|
||||
name: "true->false",
|
||||
m: newHeaderExactMatcher("th", "tv"),
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
},
|
||||
{
|
||||
name: "false->true",
|
||||
m: newHeaderExactMatcher("th", "abc"),
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := newInvertMatcher(tt.m).match(tt.md)
|
||||
want := !tt.m.match(tt.md)
|
||||
if got != want {
|
||||
t.Errorf("match() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
102
xds/internal/balancer/xdsrouting/matcher_path.go
Normal file
102
xds/internal/balancer/xdsrouting/matcher_path.go
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsrouting
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type pathMatcherInterface interface {
|
||||
match(path string) bool
|
||||
equal(pathMatcherInterface) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type pathExactMatcher struct {
|
||||
fullPath string
|
||||
}
|
||||
|
||||
func newPathExactMatcher(p string) *pathExactMatcher {
|
||||
return &pathExactMatcher{fullPath: p}
|
||||
}
|
||||
|
||||
func (pem *pathExactMatcher) match(path string) bool {
|
||||
return pem.fullPath == path
|
||||
}
|
||||
|
||||
func (pem *pathExactMatcher) equal(m pathMatcherInterface) bool {
|
||||
mm, ok := m.(*pathExactMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return pem.fullPath == mm.fullPath
|
||||
}
|
||||
|
||||
func (pem *pathExactMatcher) String() string {
|
||||
return "pathExact:" + pem.fullPath
|
||||
}
|
||||
|
||||
type pathPrefixMatcher struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func newPathPrefixMatcher(p string) *pathPrefixMatcher {
|
||||
return &pathPrefixMatcher{prefix: p}
|
||||
}
|
||||
|
||||
func (ppm *pathPrefixMatcher) match(path string) bool {
|
||||
return strings.HasPrefix(path, ppm.prefix)
|
||||
}
|
||||
|
||||
func (ppm *pathPrefixMatcher) equal(m pathMatcherInterface) bool {
|
||||
mm, ok := m.(*pathPrefixMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return ppm.prefix == mm.prefix
|
||||
}
|
||||
|
||||
func (ppm *pathPrefixMatcher) String() string {
|
||||
return "pathPrefix:" + ppm.prefix
|
||||
}
|
||||
|
||||
type pathRegexMatcher struct {
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func newPathRegexMatcher(re *regexp.Regexp) *pathRegexMatcher {
|
||||
return &pathRegexMatcher{re: re}
|
||||
}
|
||||
|
||||
func (prm *pathRegexMatcher) match(path string) bool {
|
||||
return prm.re.MatchString(path)
|
||||
}
|
||||
|
||||
func (prm *pathRegexMatcher) equal(m pathMatcherInterface) bool {
|
||||
mm, ok := m.(*pathRegexMatcher)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return prm.re.String() == mm.re.String()
|
||||
}
|
||||
|
||||
func (prm *pathRegexMatcher) String() string {
|
||||
return "pathRegex:" + prm.re.String()
|
||||
}
|
84
xds/internal/balancer/xdsrouting/matcher_path_test.go
Normal file
84
xds/internal/balancer/xdsrouting/matcher_path_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsrouting
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathFullMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fullPath string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{name: "match", fullPath: "/s/m", path: "/s/m", want: true},
|
||||
{name: "not match", fullPath: "/s/m", path: "/a/b", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fpm := newPathExactMatcher(tt.fullPath)
|
||||
if got := fpm.match(tt.path); got != tt.want {
|
||||
t.Errorf("{%q}.match(%q) = %v, want %v", tt.fullPath, tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathPrefixMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{name: "match", prefix: "/s/", path: "/s/m", want: true},
|
||||
{name: "not match", prefix: "/s/", path: "/a/b", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fpm := newPathPrefixMatcher(tt.prefix)
|
||||
if got := fpm.match(tt.path); got != tt.want {
|
||||
t.Errorf("{%q}.match(%q) = %v, want %v", tt.prefix, tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathRegexMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
regexPath string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{name: "match", regexPath: "^/s+/m.*$", path: "/sss/me", want: true},
|
||||
{name: "not match", regexPath: "^/s+/m*$", path: "/sss/b", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fpm := newPathRegexMatcher(regexp.MustCompile(tt.regexPath))
|
||||
if got := fpm.match(tt.path); got != tt.want {
|
||||
t.Errorf("{%q}.match(%q) = %v, want %v", tt.regexPath, tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
148
xds/internal/balancer/xdsrouting/matcher_test.go
Normal file
148
xds/internal/balancer/xdsrouting/matcher_test.go
Normal file
@ -0,0 +1,148 @@
|
||||
/*
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package xdsrouting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/internal/grpcrand"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestAndMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pm pathMatcherInterface
|
||||
hm headerMatcherInterface
|
||||
info balancer.PickInfo
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "both match",
|
||||
pm: newPathExactMatcher("/a/b"),
|
||||
hm: newHeaderExactMatcher("th", "tv"),
|
||||
info: balancer.PickInfo{
|
||||
FullMethodName: "/a/b",
|
||||
Ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only one match",
|
||||
pm: newPathExactMatcher("/a/b"),
|
||||
hm: newHeaderExactMatcher("th", "tv"),
|
||||
info: balancer.PickInfo{
|
||||
FullMethodName: "/z/y",
|
||||
Ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "both not match",
|
||||
pm: newPathExactMatcher("/z/y"),
|
||||
hm: newHeaderExactMatcher("th", "abc"),
|
||||
info: balancer.PickInfo{
|
||||
FullMethodName: "/a/b",
|
||||
Ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("th", "tv")),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := newCompositeMatcher(tt.pm, []headerMatcherInterface{tt.hm}, nil)
|
||||
if got := a.match(tt.info); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFractionMatcherMatch(t *testing.T) {
|
||||
const fraction = 500000
|
||||
fm := newFractionMatcher(fraction)
|
||||
defer func() {
|
||||
grpcrandInt63n = grpcrand.Int63n
|
||||
}()
|
||||
|
||||
// rand > fraction, should return false.
|
||||
grpcrandInt63n = func(n int64) int64 {
|
||||
return fraction + 1
|
||||
}
|
||||
if matched := fm.match(); matched {
|
||||
t.Errorf("match() = %v, want not match", matched)
|
||||
}
|
||||
|
||||
// rand == fraction, should return true.
|
||||
grpcrandInt63n = func(n int64) int64 {
|
||||
return fraction
|
||||
}
|
||||
if matched := fm.match(); !matched {
|
||||
t.Errorf("match() = %v, want match", matched)
|
||||
}
|
||||
|
||||
// rand < fraction, should return true.
|
||||
grpcrandInt63n = func(n int64) int64 {
|
||||
return fraction - 1
|
||||
}
|
||||
if matched := fm.match(); !matched {
|
||||
t.Errorf("match() = %v, want match", matched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompositeMatcherEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pm pathMatcherInterface
|
||||
hms []headerMatcherInterface
|
||||
fm *fractionMatcher
|
||||
mm *compositeMatcher
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal",
|
||||
pm: newPathExactMatcher("/a/b"),
|
||||
mm: newCompositeMatcher(newPathExactMatcher("/a/b"), nil, nil),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no path matcher",
|
||||
pm: nil,
|
||||
mm: newCompositeMatcher(nil, nil, nil),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not equal",
|
||||
pm: newPathExactMatcher("/a/b"),
|
||||
fm: newFractionMatcher(123),
|
||||
mm: newCompositeMatcher(newPathExactMatcher("/a/b"), nil, nil),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := newCompositeMatcher(tt.pm, tt.hms, tt.fm)
|
||||
if got := a.equal(tt.mm); got != tt.want {
|
||||
t.Errorf("equal() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user