Skip to content

Commit fd6f585

Browse files
authored
Cherry-pick #8159 and #8243 to v1.72.x (#8255)
1 parent 79ca174 commit fd6f585

File tree

21 files changed

+1154
-189
lines changed

21 files changed

+1154
-189
lines changed

internal/envconfig/envconfig.go

+14
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ var (
5555
// setting the environment variable "GRPC_EXPERIMENTAL_ENABLE_NEW_PICK_FIRST"
5656
// to "false".
5757
NewPickFirstEnabled = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_NEW_PICK_FIRST", true)
58+
59+
// XDSEndpointHashKeyBackwardCompat controls the parsing of the endpoint hash
60+
// key from EDS LbEndpoint metadata. Endpoint hash keys can be disabled by
61+
// setting "GRPC_XDS_ENDPOINT_HASH_KEY_BACKWARD_COMPAT" to "true". When the
62+
// implementation of A76 is stable, we will flip the default value to false
63+
// in a subsequent release. A final release will remove this environment
64+
// variable, enabling the new behavior unconditionally.
65+
XDSEndpointHashKeyBackwardCompat = boolFromEnv("GRPC_XDS_ENDPOINT_HASH_KEY_BACKWARD_COMPAT", true)
66+
67+
// RingHashSetRequestHashKey is set if the ring hash balancer can get the
68+
// request hash header by setting the "requestHashHeader" field, according
69+
// to gRFC A76. It can be enabled by setting the environment variable
70+
// "GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY" to "true".
71+
RingHashSetRequestHashKey = boolFromEnv("GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY", false)
5872
)
5973

6074
func boolFromEnv(envVar string, def bool) bool {

internal/metadata/metadata.go

+19-7
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,11 @@ func hasNotPrintable(msg string) bool {
9797
return false
9898
}
9999

100-
// ValidatePair validate a key-value pair with the following rules (the pseudo-header will be skipped) :
101-
//
102-
// - key must contain one or more characters.
103-
// - the characters in the key must be contained in [0-9 a-z _ - .].
104-
// - if the key ends with a "-bin" suffix, no validation of the corresponding value is performed.
105-
// - the characters in the every value must be printable (in [%x20-%x7E]).
106-
func ValidatePair(key string, vals ...string) error {
100+
// ValidateKey validates a key with the following rules (pseudo-headers are
101+
// skipped):
102+
// - the key must contain one or more characters.
103+
// - the characters in the key must be in [0-9 a-z _ - .].
104+
func ValidateKey(key string) error {
107105
// key should not be empty
108106
if key == "" {
109107
return fmt.Errorf("there is an empty key in the header")
@@ -119,6 +117,20 @@ func ValidatePair(key string, vals ...string) error {
119117
return fmt.Errorf("header key %q contains illegal characters not in [0-9a-z-_.]", key)
120118
}
121119
}
120+
return nil
121+
}
122+
123+
// ValidatePair validates a key-value pair with the following rules
124+
// (pseudo-header are skipped):
125+
// - the key must contain one or more characters.
126+
// - the characters in the key must be in [0-9 a-z _ - .].
127+
// - if the key ends with a "-bin" suffix, no validation of the corresponding
128+
// value is performed.
129+
// - the characters in every value must be printable (in [%x20-%x7E]).
130+
func ValidatePair(key string, vals ...string) error {
131+
if err := ValidateKey(key); err != nil {
132+
return err
133+
}
122134
if strings.HasSuffix(key, "-bin") {
123135
return nil
124136
}

internal/testutils/envconfig.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package testutils
2+
3+
/*
4+
*
5+
* Copyright 2025 gRPC authors.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import (
21+
"testing"
22+
)
23+
24+
// SetEnvConfig sets the value of the given variable to the specified value,
25+
// taking care of restoring the original value after the test completes.
26+
func SetEnvConfig[T any](t *testing.T, variable *T, value T) {
27+
t.Helper()
28+
old := *variable
29+
t.Cleanup(func() {
30+
*variable = old
31+
})
32+
*variable = value
33+
}

internal/testutils/xds/e2e/clientresources.go

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
2727
"google.golang.org/protobuf/proto"
2828
"google.golang.org/protobuf/types/known/anypb"
29+
"google.golang.org/protobuf/types/known/structpb"
2930
"google.golang.org/protobuf/types/known/wrapperspb"
3031

3132
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
@@ -649,6 +650,9 @@ type BackendOptions struct {
649650
HealthStatus v3corepb.HealthStatus
650651
// Weight sets the backend weight. Defaults to 1.
651652
Weight uint32
653+
// Metadata sets the LB endpoint metadata (envoy.lb FilterMetadata field).
654+
// See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/base.proto#envoy-v3-api-msg-config-core-v3-metadata
655+
Metadata map[string]any
652656
}
653657

654658
// EndpointOptions contains options to configure an Endpoint (or
@@ -708,6 +712,10 @@ func EndpointResourceWithOptions(opts EndpointOptions) *v3endpointpb.ClusterLoad
708712
},
709713
}
710714
}
715+
metadata, err := structpb.NewStruct(b.Metadata)
716+
if err != nil {
717+
panic(err)
718+
}
711719
lbEndpoints = append(lbEndpoints, &v3endpointpb.LbEndpoint{
712720
HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{Endpoint: &v3endpointpb.Endpoint{
713721
Address: &v3corepb.Address{Address: &v3corepb.Address_SocketAddress{
@@ -721,6 +729,11 @@ func EndpointResourceWithOptions(opts EndpointOptions) *v3endpointpb.ClusterLoad
721729
}},
722730
HealthStatus: b.HealthStatus,
723731
LoadBalancingWeight: &wrapperspb.UInt32Value{Value: b.Weight},
732+
Metadata: &v3corepb.Metadata{
733+
FilterMetadata: map[string]*structpb.Struct{
734+
"envoy.lb": metadata,
735+
},
736+
},
724737
})
725738
}
726739

resolver/ringhash/attr.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
*
3+
* Copyright 2025 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
// Package ringhash implements resolver related functions for the ring_hash
20+
// load balancing policy.
21+
package ringhash
22+
23+
import (
24+
"google.golang.org/grpc/resolver"
25+
)
26+
27+
type hashKeyType string
28+
29+
// hashKeyKey is the key to store the ring hash key attribute in
30+
// a resolver.Endpoint attribute.
31+
const hashKeyKey = hashKeyType("grpc.resolver.ringhash.hash_key")
32+
33+
// SetHashKey sets the hash key for this endpoint. Combined with the ring_hash
34+
// load balancing policy, it allows placing the endpoint on the ring based on an
35+
// arbitrary string instead of the IP address. If hashKey is empty, the endpoint
36+
// is returned unmodified.
37+
//
38+
// # Experimental
39+
//
40+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
41+
// later release.
42+
func SetHashKey(endpoint resolver.Endpoint, hashKey string) resolver.Endpoint {
43+
if hashKey == "" {
44+
return endpoint
45+
}
46+
endpoint.Attributes = endpoint.Attributes.WithValue(hashKeyKey, hashKey)
47+
return endpoint
48+
}
49+
50+
// HashKey returns the hash key attribute of endpoint. If this attribute is
51+
// not set, it returns the empty string.
52+
//
53+
// # Experimental
54+
//
55+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
56+
// later release.
57+
func HashKey(endpoint resolver.Endpoint) string {
58+
hashKey, _ := endpoint.Attributes.Value(hashKeyKey).(string)
59+
return hashKey
60+
}

xds/internal/balancer/clusterresolver/configbuilder.go

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"google.golang.org/grpc/internal/hierarchy"
2828
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
2929
"google.golang.org/grpc/resolver"
30+
"google.golang.org/grpc/resolver/ringhash"
3031
"google.golang.org/grpc/xds/internal"
3132
"google.golang.org/grpc/xds/internal/balancer/clusterimpl"
3233
"google.golang.org/grpc/xds/internal/balancer/outlierdetection"
@@ -284,6 +285,7 @@ func priorityLocalitiesToClusterImpl(localities []xdsresource.Locality, priority
284285
ew = endpoint.Weight
285286
}
286287
resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: lw * ew})
288+
resolverEndpoint = ringhash.SetHashKey(resolverEndpoint, endpoint.HashKey)
287289
retEndpoints = append(retEndpoints, resolverEndpoint)
288290
}
289291
}

xds/internal/balancer/ringhash/config.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@ package ringhash
2121
import (
2222
"encoding/json"
2323
"fmt"
24+
"strings"
2425

2526
"google.golang.org/grpc/internal/envconfig"
27+
"google.golang.org/grpc/internal/metadata"
2628
"google.golang.org/grpc/serviceconfig"
2729
)
2830

2931
// LBConfig is the balancer config for ring_hash balancer.
3032
type LBConfig struct {
3133
serviceconfig.LoadBalancingConfig `json:"-"`
3234

33-
MinRingSize uint64 `json:"minRingSize,omitempty"`
34-
MaxRingSize uint64 `json:"maxRingSize,omitempty"`
35+
MinRingSize uint64 `json:"minRingSize,omitempty"`
36+
MaxRingSize uint64 `json:"maxRingSize,omitempty"`
37+
RequestHashHeader string `json:"requestHashHeader,omitempty"`
3538
}
3639

3740
const (
@@ -66,5 +69,18 @@ func parseConfig(c json.RawMessage) (*LBConfig, error) {
6669
if cfg.MaxRingSize > envconfig.RingHashCap {
6770
cfg.MaxRingSize = envconfig.RingHashCap
6871
}
72+
if !envconfig.RingHashSetRequestHashKey {
73+
cfg.RequestHashHeader = ""
74+
}
75+
if cfg.RequestHashHeader != "" {
76+
cfg.RequestHashHeader = strings.ToLower(cfg.RequestHashHeader)
77+
// See rules in https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md#explicitly-setting-the-request-hash-key
78+
if err := metadata.ValidateKey(cfg.RequestHashHeader); err != nil {
79+
return nil, fmt.Errorf("invalid requestHashHeader %q: %v", cfg.RequestHashHeader, err)
80+
}
81+
if strings.HasSuffix(cfg.RequestHashHeader, "-bin") {
82+
return nil, fmt.Errorf("invalid requestHashHeader %q: key must not end with \"-bin\"", cfg.RequestHashHeader)
83+
}
84+
}
6985
return &cfg, nil
7086
}

0 commit comments

Comments
 (0)