Skip to content

Commit 573e5e4

Browse files
committed
ringhash: implement gRFC A76
- Add the ability to set the request hash header. This allows using ring hash without xDS. - Add the ability to specify the location of endpoints on the ring, and a default implementation based on EDS metadata that matches Envoy behavior. See https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md for details. Release Notes: - ringhash: implement gRFC A76.
1 parent 0914bba commit 573e5e4

File tree

21 files changed

+857
-116
lines changed

21 files changed

+857
-116
lines changed

internal/envconfig/envconfig.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ 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 disables parsing of the endpoint hash
60+
// key from EDS LbEndpoint metadata. We can disable this behavior by setting
61+
// the environment variable "GRPC_XDS_ENDPOINT_HASH_KEY_BACKWARD_COMPAT" to
62+
// "true".
63+
XDSEndpointHashKeyBackwardCompat = boolFromEnv("GRPC_XDS_ENDPOINT_HASH_KEY_BACKWARD_COMPAT", true)
64+
65+
// RingHashSetRequestHashKey is set if the ring hash balancer can get the
66+
// request hash header by setting the "request_hash_header" field, according
67+
// to gRFC A76. It can be enabled by setting the environment variable
68+
// "GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY" to "true".
69+
RingHashSetRequestHashKey = boolFromEnv("GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY", false)
5870
)
5971

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

internal/metadata/metadata.go

Lines changed: 19 additions & 7 deletions
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/xds/e2e/clientresources.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"net"
2424
"strconv"
2525

26+
"google.golang.org/protobuf/types/known/structpb"
27+
2628
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
2729
"google.golang.org/protobuf/proto"
2830
"google.golang.org/protobuf/types/known/anypb"
@@ -715,6 +717,9 @@ type BackendOptions struct {
715717
HealthStatus v3corepb.HealthStatus
716718
// Weight sets the backend weight. Defaults to 1.
717719
Weight uint32
720+
// Metadata sets the LB endpoint metadata (envoy.lb FilterMetadata field).
721+
// See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/base.proto#envoy-v3-api-msg-config-core-v3-metadata
722+
Metadata map[string]any
718723
}
719724

720725
// EndpointOptions contains options to configure an Endpoint (or
@@ -774,6 +779,10 @@ func EndpointResourceWithOptions(opts EndpointOptions) *v3endpointpb.ClusterLoad
774779
},
775780
}
776781
}
782+
metadata, err := structpb.NewStruct(b.Metadata)
783+
if err != nil {
784+
panic(err)
785+
}
777786
lbEndpoints = append(lbEndpoints, &v3endpointpb.LbEndpoint{
778787
HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{Endpoint: &v3endpointpb.Endpoint{
779788
Address: &v3corepb.Address{Address: &v3corepb.Address_SocketAddress{
@@ -787,6 +796,11 @@ func EndpointResourceWithOptions(opts EndpointOptions) *v3endpointpb.ClusterLoad
787796
}},
788797
HealthStatus: b.HealthStatus,
789798
LoadBalancingWeight: &wrapperspb.UInt32Value{Value: b.Weight},
799+
Metadata: &v3corepb.Metadata{
800+
FilterMetadata: map[string]*structpb.Struct{
801+
"envoy.lb": metadata,
802+
},
803+
},
790804
})
791805
}
792806

resolver/ringhash/attr.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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("hash_key")
32+
33+
// SetEndpointHashKey sets the hash key for this endpoint. Combined with the
34+
// ring_hash load balancing policy, it allows placing the endpoint on the ring
35+
// based on an arbitrary string instead of the IP address.
36+
//
37+
// # Experimental
38+
//
39+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
40+
// later release.
41+
func SetEndpointHashKey(ep resolver.Endpoint, hashKey string) resolver.Endpoint {
42+
if hashKey == "" {
43+
return ep
44+
}
45+
ep.Attributes = ep.Attributes.WithValue(hashKeyKey, hashKey)
46+
return ep
47+
}
48+
49+
// GetEndpointHashKey returns the hash key attribute of addr. If this attribute
50+
// is not set, it returns the empty string.
51+
//
52+
// # Experimental
53+
//
54+
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
55+
// later release.
56+
func GetEndpointHashKey(ep resolver.Endpoint) string {
57+
hashKey, _ := ep.Attributes.Value(hashKeyKey).(string)
58+
return hashKey
59+
}

xds/internal/balancer/clusterresolver/configbuilder.go

Lines changed: 2 additions & 0 deletions
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"
@@ -282,6 +283,7 @@ func priorityLocalitiesToClusterImpl(localities []xdsresource.Locality, priority
282283
ew = endpoint.Weight
283284
}
284285
resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: lw * ew})
286+
resolverEndpoint = ringhash.SetEndpointHashKey(resolverEndpoint, endpoint.HashKey)
285287
retEndpoints = append(retEndpoints, resolverEndpoint)
286288
}
287289
}

xds/internal/balancer/ringhash/config.go

Lines changed: 17 additions & 2 deletions
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:"request_hash_header,omitempty"`
3538
}
3639

3740
const (
@@ -66,5 +69,17 @@ 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+
// See rules in https://github.com/grpc/proposal/blob/b64e6d3953816ed3b16b88bde0b7c16d3b62654f/A76-ring-hash-improvements.md#explicitly-setting-the-request-hash-key
77+
if err := metadata.ValidateKey(cfg.RequestHashHeader); err != nil {
78+
return nil, fmt.Errorf("invalid request_metadata_key %q: %s", cfg.RequestHashHeader, err)
79+
}
80+
if strings.HasSuffix(cfg.RequestHashHeader, "-bin") {
81+
return nil, fmt.Errorf("invalid request_metadata_key %q: key must not end with \"-bin\"", cfg.RequestHashHeader)
82+
}
83+
}
6984
return &cfg, nil
7085
}

xds/internal/balancer/ringhash/config_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import (
2626
)
2727

2828
func (s) TestParseConfig(t *testing.T) {
29+
oldEnvConfig := envconfig.RingHashSetRequestHashKey
30+
defer func() { envconfig.RingHashSetRequestHashKey = oldEnvConfig }()
31+
envconfig.RingHashSetRequestHashKey = true
32+
2933
tests := []struct {
3034
name string
3135
js string
@@ -94,6 +98,27 @@ func (s) TestParseConfig(t *testing.T) {
9498
want: nil,
9599
wantErr: true,
96100
},
101+
{
102+
name: "request metadata key set",
103+
js: `{"request_hash_header": "x-foo"}`,
104+
want: &LBConfig{
105+
MinRingSize: defaultMinSize,
106+
MaxRingSize: defaultMaxSize,
107+
RequestHashHeader: "x-foo",
108+
},
109+
},
110+
{
111+
name: "invalid request hash header",
112+
js: `{"request_hash_header": "!invalid"}`,
113+
want: nil,
114+
wantErr: true,
115+
},
116+
{
117+
name: "binary request hash header",
118+
js: `{"request_hash_header": "header-with-bin"}`,
119+
want: nil,
120+
wantErr: true,
121+
},
97122
}
98123
for _, tt := range tests {
99124
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)