diff --git a/xds/internal/balancer/xdsrouting/doc.go b/xds/internal/balancer/xdsrouting/doc.go new file mode 100644 index 00000000..2f00649a --- /dev/null +++ b/xds/internal/balancer/xdsrouting/doc.go @@ -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 diff --git a/xds/internal/balancer/xdsrouting/matcher.go b/xds/internal/balancer/xdsrouting/matcher.go new file mode 100644 index 00000000..79fab828 --- /dev/null +++ b/xds/internal/balancer/xdsrouting/matcher.go @@ -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) +} diff --git a/xds/internal/balancer/xdsrouting/matcher_header.go b/xds/internal/balancer/xdsrouting/matcher_header.go new file mode 100644 index 00000000..a900e43f --- /dev/null +++ b/xds/internal/balancer/xdsrouting/matcher_header.go @@ -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) +} diff --git a/xds/internal/balancer/xdsrouting/matcher_header_test.go b/xds/internal/balancer/xdsrouting/matcher_header_test.go new file mode 100644 index 00000000..3ec73ee9 --- /dev/null +++ b/xds/internal/balancer/xdsrouting/matcher_header_test.go @@ -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) + } + }) + } +} diff --git a/xds/internal/balancer/xdsrouting/matcher_path.go b/xds/internal/balancer/xdsrouting/matcher_path.go new file mode 100644 index 00000000..9c783acb --- /dev/null +++ b/xds/internal/balancer/xdsrouting/matcher_path.go @@ -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() +} diff --git a/xds/internal/balancer/xdsrouting/matcher_path_test.go b/xds/internal/balancer/xdsrouting/matcher_path_test.go new file mode 100644 index 00000000..ad7db748 --- /dev/null +++ b/xds/internal/balancer/xdsrouting/matcher_path_test.go @@ -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) + } + }) + } +} diff --git a/xds/internal/balancer/xdsrouting/matcher_test.go b/xds/internal/balancer/xdsrouting/matcher_test.go new file mode 100644 index 00000000..8c8a4b52 --- /dev/null +++ b/xds/internal/balancer/xdsrouting/matcher_test.go @@ -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) + } + }) + } +}