Skip to content

Commit b12df2f

Browse files
Merge branch 'master' into joy.bestourous/header-check
2 parents 23406a6 + 489f1f6 commit b12df2f

7 files changed

Lines changed: 566 additions & 31 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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 randomsubsetting implements the random_subsetting LB policy specified
20+
// here: https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md
21+
//
22+
// To install the LB policy, import this package as:
23+
//
24+
// import _ "google.golang.org/grpc/balancer/randomsubsetting"
25+
//
26+
// # Experimental
27+
//
28+
// Notice: This package is EXPERIMENTAL and may be changed or removed in a
29+
// later release.
30+
package randomsubsetting
31+
32+
import (
33+
"cmp"
34+
"encoding/json"
35+
"fmt"
36+
"math/rand/v2"
37+
"slices"
38+
39+
xxhash "github.com/cespare/xxhash/v2"
40+
"google.golang.org/grpc/balancer"
41+
"google.golang.org/grpc/grpclog"
42+
"google.golang.org/grpc/internal/balancer/gracefulswitch"
43+
internalgrpclog "google.golang.org/grpc/internal/grpclog"
44+
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
45+
"google.golang.org/grpc/resolver"
46+
"google.golang.org/grpc/serviceconfig"
47+
)
48+
49+
// Name is the name of the random subsetting load balancer.
50+
const Name = "random_subsetting_experimental"
51+
52+
var (
53+
logger = grpclog.Component(Name)
54+
randUint64 = rand.Uint64
55+
)
56+
57+
func prefixLogger(p *subsettingBalancer) *internalgrpclog.PrefixLogger {
58+
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[random-subsetting-lb %p] ", p))
59+
}
60+
61+
func init() {
62+
balancer.Register(bb{})
63+
}
64+
65+
type bb struct{}
66+
67+
func (bb) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer {
68+
b := &subsettingBalancer{
69+
Balancer: gracefulswitch.NewBalancer(cc, bOpts),
70+
hashSeed: randUint64(),
71+
hashDigest: xxhash.New(),
72+
}
73+
b.logger = prefixLogger(b)
74+
b.logger.Infof("Created")
75+
return b
76+
}
77+
78+
type lbConfig struct {
79+
serviceconfig.LoadBalancingConfig `json:"-"`
80+
81+
SubsetSize uint32 `json:"subsetSize,omitempty"`
82+
ChildPolicy *iserviceconfig.BalancerConfig `json:"childPolicy,omitempty"`
83+
}
84+
85+
func (bb) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
86+
lbCfg := &lbConfig{}
87+
88+
// Ensure that the specified child policy is registered and validates its
89+
// config, if present.
90+
if err := json.Unmarshal(s, lbCfg); err != nil {
91+
return nil, fmt.Errorf("randomsubsetting: json.Unmarshal failed for configuration: %s with error: %v", string(s), err)
92+
}
93+
if lbCfg.SubsetSize == 0 {
94+
return nil, fmt.Errorf("randomsubsetting: SubsetSize must be greater than 0")
95+
}
96+
if lbCfg.ChildPolicy == nil {
97+
return nil, fmt.Errorf("randomsubsetting: ChildPolicy must be specified")
98+
}
99+
100+
return lbCfg, nil
101+
}
102+
103+
func (bb) Name() string {
104+
return Name
105+
}
106+
107+
type subsettingBalancer struct {
108+
*gracefulswitch.Balancer
109+
110+
logger *internalgrpclog.PrefixLogger
111+
cfg *lbConfig
112+
hashSeed uint64
113+
hashDigest *xxhash.Digest
114+
}
115+
116+
func (b *subsettingBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
117+
lbCfg, ok := s.BalancerConfig.(*lbConfig)
118+
if !ok {
119+
b.logger.Warningf("Received config with unexpected type %T: %v", s.BalancerConfig, s.BalancerConfig)
120+
return balancer.ErrBadResolverState
121+
}
122+
123+
// Build config for the gracefulswitch balancer. It is safe to ignore
124+
// JSON marshaling errors here, since the config was already validated
125+
// as part of ParseConfig().
126+
cfg := []map[string]any{{lbCfg.ChildPolicy.Name: lbCfg.ChildPolicy.Config}}
127+
cfgJSON, _ := json.Marshal(cfg)
128+
parsedCfg, err := gracefulswitch.ParseConfig(cfgJSON)
129+
if err != nil {
130+
return fmt.Errorf("randomsubsetting: error switching to child of type %q: %v", lbCfg.ChildPolicy.Name, err)
131+
}
132+
b.cfg = lbCfg
133+
endpoints := resolver.State{
134+
Endpoints: b.calculateSubset(s.ResolverState.Endpoints),
135+
ServiceConfig: s.ResolverState.ServiceConfig,
136+
Attributes: s.ResolverState.Attributes,
137+
}
138+
139+
return b.Balancer.UpdateClientConnState(balancer.ClientConnState{
140+
ResolverState: endpoints,
141+
BalancerConfig: parsedCfg,
142+
})
143+
}
144+
145+
// calculateSubset implements the subsetting algorithm, as described in A68:
146+
// https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md#subsetting-algorithm
147+
func (b *subsettingBalancer) calculateSubset(endpoints []resolver.Endpoint) []resolver.Endpoint {
148+
// A helper struct to hold an endpoint and its hash.
149+
type endpointWithHash struct {
150+
hash uint64
151+
ep resolver.Endpoint
152+
}
153+
154+
subsetSize := b.cfg.SubsetSize
155+
if len(endpoints) <= int(subsetSize) {
156+
return endpoints
157+
}
158+
159+
hashedEndpoints := make([]endpointWithHash, len(endpoints))
160+
for i, endpoint := range endpoints {
161+
// For every endpoint in the list, compute a hash with previously
162+
// generated seed - A68.
163+
//
164+
// The xxhash package's Sum64() function does not allow setting a seed.
165+
// This means that we need to reset the digest with the seed for every
166+
// endpoint. Without this, an endpoint will not retain the same hash
167+
// across resolver updates.
168+
//
169+
// Note that we only hash the first address of the endpoint, as per A68.
170+
b.hashDigest.ResetWithSeed(b.hashSeed)
171+
b.hashDigest.WriteString(endpoint.Addresses[0].String())
172+
hashedEndpoints[i] = endpointWithHash{
173+
hash: b.hashDigest.Sum64(),
174+
ep: endpoint,
175+
}
176+
}
177+
178+
slices.SortFunc(hashedEndpoints, func(a, b endpointWithHash) int {
179+
// Note: This uses the standard library cmp package, not the
180+
// github.com/google/go-cmp/cmp package. The latter is intended for
181+
// testing purposes only.
182+
return cmp.Compare(a.hash, b.hash)
183+
})
184+
185+
// Convert back to resolver.Endpoints
186+
endpointSubset := make([]resolver.Endpoint, subsetSize)
187+
for i, endpoint := range hashedEndpoints[:subsetSize] {
188+
endpointSubset[i] = endpoint.ep
189+
}
190+
191+
return endpointSubset
192+
}

0 commit comments

Comments
 (0)