From e2f575e56f2eafd0400d1385e15b89ddc4564fbf Mon Sep 17 00:00:00 2001
From: Menghan Li <menghanl@google.com>
Date: Thu, 16 Jul 2020 16:05:47 -0700
Subject: [PATCH] xdsrouting: all matchers (#3733)

---
 xds/internal/balancer/xdsrouting/doc.go       |  20 ++
 xds/internal/balancer/xdsrouting/matcher.go   | 134 +++++++
 .../balancer/xdsrouting/matcher_header.go     | 245 +++++++++++++
 .../xdsrouting/matcher_header_test.go         | 333 ++++++++++++++++++
 .../balancer/xdsrouting/matcher_path.go       | 102 ++++++
 .../balancer/xdsrouting/matcher_path_test.go  |  84 +++++
 .../balancer/xdsrouting/matcher_test.go       | 148 ++++++++
 7 files changed, 1066 insertions(+)
 create mode 100644 xds/internal/balancer/xdsrouting/doc.go
 create mode 100644 xds/internal/balancer/xdsrouting/matcher.go
 create mode 100644 xds/internal/balancer/xdsrouting/matcher_header.go
 create mode 100644 xds/internal/balancer/xdsrouting/matcher_header_test.go
 create mode 100644 xds/internal/balancer/xdsrouting/matcher_path.go
 create mode 100644 xds/internal/balancer/xdsrouting/matcher_path_test.go
 create mode 100644 xds/internal/balancer/xdsrouting/matcher_test.go

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)
+			}
+		})
+	}
+}