Skip to content

Commit 22c5358

Browse files
authored
xds: add HashPolicy fields to RDS update (#4521)
* Add HashPolicy fields to RDS update
1 parent 4554924 commit 22c5358

File tree

4 files changed

+242
-2
lines changed

4 files changed

+242
-2
lines changed

internal/xds/env/env.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const (
3939
// When both bootstrap FileName and FileContent are set, FileName is used.
4040
BootstrapFileContentEnv = "GRPC_XDS_BOOTSTRAP_CONFIG"
4141

42+
ringHashSupportEnv = "GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH"
4243
clientSideSecuritySupportEnv = "GRPC_XDS_EXPERIMENTAL_SECURITY_SUPPORT"
4344
aggregateAndDNSSupportEnv = "GRPC_XDS_EXPERIMENTAL_ENABLE_AGGREGATE_AND_LOGICAL_DNS_CLUSTER"
4445

@@ -59,7 +60,10 @@ var (
5960
//
6061
// When both bootstrap FileName and FileContent are set, FileName is used.
6162
BootstrapFileContent = os.Getenv(BootstrapFileContentEnv)
62-
63+
// RingHashSupport indicates whether ring hash support is enabled, which can
64+
// be enabled by setting the environment variable
65+
// "GRPC_XDS_EXPERIMENTAL_ENABLE_RING_HASH" to "true".
66+
RingHashSupport = strings.EqualFold(os.Getenv(ringHashSupportEnv), "true")
6367
// ClientSideSecuritySupport is used to control processing of security
6468
// configuration on the client-side.
6569
//

xds/internal/xdsclient/client.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,28 @@ type VirtualHost struct {
269269
HTTPFilterConfigOverride map[string]httpfilter.FilterConfig
270270
}
271271

272+
// HashPolicyType specifies the type of HashPolicy from a received RDS Response.
273+
type HashPolicyType int
274+
275+
const (
276+
// HashPolicyTypeHeader specifies to hash a Header in the incoming request.
277+
HashPolicyTypeHeader HashPolicyType = iota
278+
// HashPolicyTypeChannelID specifies to hash a unique Identifier of the
279+
// Channel. In grpc-go, this will be done using the ClientConn pointer.
280+
HashPolicyTypeChannelID
281+
)
282+
283+
// HashPolicy specifies the HashPolicy if the upstream cluster uses a hashing
284+
// load balancer.
285+
type HashPolicy struct {
286+
HashPolicyType HashPolicyType
287+
Terminal bool
288+
// Fields used for type HEADER.
289+
HeaderName string
290+
Regex *regexp.Regexp
291+
RegexSubstitution string
292+
}
293+
272294
// Route is both a specification of how to match a request as well as an
273295
// indication of the action to take upon match.
274296
type Route struct {
@@ -281,6 +303,8 @@ type Route struct {
281303
Headers []*HeaderMatcher
282304
Fraction *uint32
283305

306+
HashPolicies []*HashPolicy
307+
284308
// If the matchers above indicate a match, the below configuration is used.
285309
WeightedClusters map[string]WeightedCluster
286310
// If MaxStreamDuration is nil, it indicates neither of the route action's

xds/internal/xdsclient/rds_test.go

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/google/go-cmp/cmp"
3030
"github.com/google/go-cmp/cmp/cmpopts"
3131
"google.golang.org/grpc/internal/testutils"
32+
"google.golang.org/grpc/internal/xds/env"
3233
"google.golang.org/grpc/xds/internal/httpfilter"
3334
"google.golang.org/grpc/xds/internal/version"
3435
"google.golang.org/protobuf/types/known/durationpb"
@@ -1153,6 +1154,61 @@ func (s) TestRoutesProtoToSlice(t *testing.T) {
11531154
}},
11541155
wantErr: false,
11551156
},
1157+
{
1158+
name: "good-with-channel-id-hash-policy",
1159+
routes: []*v3routepb.Route{
1160+
{
1161+
Match: &v3routepb.RouteMatch{
1162+
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/a/"},
1163+
Headers: []*v3routepb.HeaderMatcher{
1164+
{
1165+
Name: "th",
1166+
HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{
1167+
PrefixMatch: "tv",
1168+
},
1169+
InvertMatch: true,
1170+
},
1171+
},
1172+
RuntimeFraction: &v3corepb.RuntimeFractionalPercent{
1173+
DefaultValue: &v3typepb.FractionalPercent{
1174+
Numerator: 1,
1175+
Denominator: v3typepb.FractionalPercent_HUNDRED,
1176+
},
1177+
},
1178+
},
1179+
Action: &v3routepb.Route_Route{
1180+
Route: &v3routepb.RouteAction{
1181+
ClusterSpecifier: &v3routepb.RouteAction_WeightedClusters{
1182+
WeightedClusters: &v3routepb.WeightedCluster{
1183+
Clusters: []*v3routepb.WeightedCluster_ClusterWeight{
1184+
{Name: "B", Weight: &wrapperspb.UInt32Value{Value: 60}},
1185+
{Name: "A", Weight: &wrapperspb.UInt32Value{Value: 40}},
1186+
},
1187+
TotalWeight: &wrapperspb.UInt32Value{Value: 100},
1188+
}},
1189+
HashPolicy: []*v3routepb.RouteAction_HashPolicy{
1190+
{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}}},
1191+
},
1192+
}},
1193+
},
1194+
},
1195+
wantRoutes: []*Route{{
1196+
Prefix: newStringP("/a/"),
1197+
Headers: []*HeaderMatcher{
1198+
{
1199+
Name: "th",
1200+
InvertMatch: newBoolP(true),
1201+
PrefixMatch: newStringP("tv"),
1202+
},
1203+
},
1204+
Fraction: newUInt32P(10000),
1205+
WeightedClusters: map[string]WeightedCluster{"A": {Weight: 40}, "B": {Weight: 60}},
1206+
HashPolicies: []*HashPolicy{
1207+
{HashPolicyType: HashPolicyTypeChannelID},
1208+
},
1209+
}},
1210+
wantErr: false,
1211+
},
11561212
{
11571213
name: "with custom HTTP filter config",
11581214
routes: goodRouteWithFilterConfigs(map[string]*anypb.Any{"foo": customFilterConfig}),
@@ -1197,7 +1253,9 @@ func (s) TestRoutesProtoToSlice(t *testing.T) {
11971253
return fmt.Sprint(fc)
11981254
}),
11991255
}
1200-
1256+
oldRingHashSupport := env.RingHashSupport
1257+
env.RingHashSupport = true
1258+
defer func() { env.RingHashSupport = oldRingHashSupport }()
12011259
for _, tt := range tests {
12021260
t.Run(tt.name, func(t *testing.T) {
12031261
got, err := routesProtoToSlice(tt.routes, nil, false)
@@ -1211,6 +1269,119 @@ func (s) TestRoutesProtoToSlice(t *testing.T) {
12111269
}
12121270
}
12131271

1272+
func (s) TestHashPoliciesProtoToSlice(t *testing.T) {
1273+
tests := []struct {
1274+
name string
1275+
hashPolicies []*v3routepb.RouteAction_HashPolicy
1276+
wantHashPolicies []*HashPolicy
1277+
wantErr bool
1278+
}{
1279+
// header-hash-policy tests a basic hash policy that specifies to hash a
1280+
// certain header.
1281+
{
1282+
name: "header-hash-policy",
1283+
hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1284+
{
1285+
PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
1286+
Header: &v3routepb.RouteAction_HashPolicy_Header{
1287+
HeaderName: ":path",
1288+
RegexRewrite: &v3matcherpb.RegexMatchAndSubstitute{
1289+
Pattern: &v3matcherpb.RegexMatcher{Regex: "/products"},
1290+
Substitution: "/products",
1291+
},
1292+
},
1293+
},
1294+
},
1295+
},
1296+
wantHashPolicies: []*HashPolicy{
1297+
{
1298+
HashPolicyType: HashPolicyTypeHeader,
1299+
HeaderName: ":path",
1300+
Regex: func() *regexp.Regexp { return regexp.MustCompile("/products") }(),
1301+
RegexSubstitution: "/products",
1302+
},
1303+
},
1304+
},
1305+
// channel-id-hash-policy tests a basic hash policy that specifies to
1306+
// hash a unique identifier of the channel.
1307+
{
1308+
name: "channel-id-hash-policy",
1309+
hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1310+
{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}}},
1311+
},
1312+
wantHashPolicies: []*HashPolicy{
1313+
{HashPolicyType: HashPolicyTypeChannelID},
1314+
},
1315+
},
1316+
// unsupported-filter-state-key tests that an unsupported key in the
1317+
// filter state hash policy are treated as a no-op.
1318+
{
1319+
name: "wrong-filter-state-key",
1320+
hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1321+
{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "unsupported key"}}},
1322+
},
1323+
},
1324+
// no-op-hash-policy tests that hash policies that are not supported by
1325+
// grpc are treated as a no-op.
1326+
{
1327+
name: "no-op-hash-policy",
1328+
hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1329+
{PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{}},
1330+
},
1331+
},
1332+
// header-and-channel-id-hash-policy test that a list of header and
1333+
// channel id hash policies are successfully converted to an internal
1334+
// struct.
1335+
{
1336+
name: "header-and-channel-id-hash-policy",
1337+
hashPolicies: []*v3routepb.RouteAction_HashPolicy{
1338+
{
1339+
PolicySpecifier: &v3routepb.RouteAction_HashPolicy_Header_{
1340+
Header: &v3routepb.RouteAction_HashPolicy_Header{
1341+
HeaderName: ":path",
1342+
RegexRewrite: &v3matcherpb.RegexMatchAndSubstitute{
1343+
Pattern: &v3matcherpb.RegexMatcher{Regex: "/products"},
1344+
Substitution: "/products",
1345+
},
1346+
},
1347+
},
1348+
},
1349+
{
1350+
PolicySpecifier: &v3routepb.RouteAction_HashPolicy_FilterState_{FilterState: &v3routepb.RouteAction_HashPolicy_FilterState{Key: "io.grpc.channel_id"}},
1351+
Terminal: true,
1352+
},
1353+
},
1354+
wantHashPolicies: []*HashPolicy{
1355+
{
1356+
HashPolicyType: HashPolicyTypeHeader,
1357+
HeaderName: ":path",
1358+
Regex: func() *regexp.Regexp { return regexp.MustCompile("/products") }(),
1359+
RegexSubstitution: "/products",
1360+
},
1361+
{
1362+
HashPolicyType: HashPolicyTypeChannelID,
1363+
Terminal: true,
1364+
},
1365+
},
1366+
},
1367+
}
1368+
1369+
oldRingHashSupport := env.RingHashSupport
1370+
env.RingHashSupport = true
1371+
defer func() { env.RingHashSupport = oldRingHashSupport }()
1372+
for _, tt := range tests {
1373+
t.Run(tt.name, func(t *testing.T) {
1374+
got, err := hashPoliciesProtoToSlice(tt.hashPolicies, nil)
1375+
if (err != nil) != tt.wantErr {
1376+
t.Fatalf("hashPoliciesProtoToSlice() error = %v, wantErr %v", err, tt.wantErr)
1377+
}
1378+
if diff := cmp.Diff(got, tt.wantHashPolicies, cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
1379+
t.Fatalf("hashPoliciesProtoToSlice() returned unexpected diff (-got +want):\n%s", diff)
1380+
}
1381+
})
1382+
}
1383+
}
1384+
12141385
func newStringP(s string) *string {
12151386
return &s
12161387
}

xds/internal/xdsclient/xds.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,16 @@ func routesProtoToSlice(routes []*v3routepb.Route, logger *grpclog.PrefixLogger,
496496

497497
route.WeightedClusters = make(map[string]WeightedCluster)
498498
action := r.GetRoute()
499+
500+
// Hash Policies are only applicable for a Ring Hash LB.
501+
if env.RingHashSupport {
502+
hp, err := hashPoliciesProtoToSlice(action.HashPolicy, logger)
503+
if err != nil {
504+
return nil, err
505+
}
506+
route.HashPolicies = hp
507+
}
508+
499509
switch a := action.GetClusterSpecifier().(type) {
500510
case *v3routepb.RouteAction_Cluster:
501511
route.WeightedClusters[a.Cluster] = WeightedCluster{Weight: 1}
@@ -557,6 +567,37 @@ func routesProtoToSlice(routes []*v3routepb.Route, logger *grpclog.PrefixLogger,
557567
return routesRet, nil
558568
}
559569

570+
func hashPoliciesProtoToSlice(policies []*v3routepb.RouteAction_HashPolicy, logger *grpclog.PrefixLogger) ([]*HashPolicy, error) {
571+
var hashPoliciesRet []*HashPolicy
572+
for _, p := range policies {
573+
policy := HashPolicy{Terminal: p.Terminal}
574+
switch p.GetPolicySpecifier().(type) {
575+
case *v3routepb.RouteAction_HashPolicy_Header_:
576+
policy.HashPolicyType = HashPolicyTypeHeader
577+
policy.HeaderName = p.GetHeader().GetHeaderName()
578+
regex := p.GetHeader().GetRegexRewrite().GetPattern().GetRegex()
579+
re, err := regexp.Compile(regex)
580+
if err != nil {
581+
return nil, fmt.Errorf("hash policy %+v contains an invalid regex %q", p, regex)
582+
}
583+
policy.Regex = re
584+
policy.RegexSubstitution = p.GetHeader().GetRegexRewrite().GetSubstitution()
585+
case *v3routepb.RouteAction_HashPolicy_FilterState_:
586+
if p.GetFilterState().GetKey() != "io.grpc.channel_id" {
587+
logger.Infof("hash policy %+v contains an invalid key for filter state policy %q", p, p.GetFilterState().GetKey())
588+
continue
589+
}
590+
policy.HashPolicyType = HashPolicyTypeChannelID
591+
default:
592+
logger.Infof("hash policy %T is an unsupported hash policy", p.GetPolicySpecifier())
593+
continue
594+
}
595+
596+
hashPoliciesRet = append(hashPoliciesRet, &policy)
597+
}
598+
return hashPoliciesRet, nil
599+
}
600+
560601
// UnmarshalCluster processes resources received in an CDS response, validates
561602
// them, and transforms them into a native struct which contains only fields we
562603
// are interested in.

0 commit comments

Comments
 (0)