Skip to content

Commit 429e6e0

Browse files
authored
balancer: expose endpoint weight and hostname as experimental APIs (#9074)
### Description This PR exposes two new **experimental** public APIs so that custom load balancing policies can access endpoint attributes that were previously only available via internal packages: - `google.golang.org/grpc/balancer/weight` — `Set`, `FromEndpoint`, `EndpointInfo` - `google.golang.org/grpc/balancer/hostname` — `Set`, `FromEndpoint` These attributes (especially `weight`) are required by custom balancers such as deterministic aperture / P2C that need EDS-provided endpoint details like weights etc. Fixes #8971 RELEASE NOTES: - experimental/balancer/hostname: New APIs to access endpoint hostname attributes - experimental/balancer/weight: New APIs to access endpoint weight attributes
1 parent 761e655 commit 429e6e0

14 files changed

Lines changed: 161 additions & 57 deletions

File tree

‎balancer/pickfirst/pickfirst.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ import (
3535
"google.golang.org/grpc/balancer"
3636
"google.golang.org/grpc/balancer/pickfirst/internal"
3737
"google.golang.org/grpc/connectivity"
38+
"google.golang.org/grpc/experimental/balancer/weight"
3839
expstats "google.golang.org/grpc/experimental/stats"
3940
"google.golang.org/grpc/grpclog"
40-
"google.golang.org/grpc/internal/balancer/weight"
4141
"google.golang.org/grpc/internal/envconfig"
4242
internalgrpclog "google.golang.org/grpc/internal/grpclog"
4343
"google.golang.org/grpc/internal/pretty"

‎balancer/pickfirst/pickfirst_ext_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ import (
3737
"google.golang.org/grpc/codes"
3838
"google.golang.org/grpc/connectivity"
3939
"google.golang.org/grpc/credentials/insecure"
40+
"google.golang.org/grpc/experimental/balancer/weight"
4041
"google.golang.org/grpc/internal"
4142
"google.golang.org/grpc/internal/balancer/stub"
42-
"google.golang.org/grpc/internal/balancer/weight"
4343
"google.golang.org/grpc/internal/channelz"
4444
"google.golang.org/grpc/internal/envconfig"
4545
"google.golang.org/grpc/internal/grpcsync"

‎balancer/ringhash/ring_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"testing"
2525

2626
xxhash "github.com/cespare/xxhash/v2"
27-
"google.golang.org/grpc/internal/balancer/weight"
27+
"google.golang.org/grpc/experimental/balancer/weight"
2828
"google.golang.org/grpc/resolver"
2929
)
3030

‎balancer/ringhash/ringhash.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import (
4242
"google.golang.org/grpc/balancer/lazy"
4343
"google.golang.org/grpc/balancer/pickfirst"
4444
"google.golang.org/grpc/connectivity"
45-
"google.golang.org/grpc/internal/balancer/weight"
45+
"google.golang.org/grpc/experimental/balancer/weight"
4646
"google.golang.org/grpc/internal/grpclog"
4747
"google.golang.org/grpc/internal/pretty"
4848
iringhash "google.golang.org/grpc/internal/ringhash"

‎balancer/ringhash/ringhash_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
"google.golang.org/grpc/balancer"
2828
"google.golang.org/grpc/connectivity"
29-
"google.golang.org/grpc/internal/balancer/weight"
29+
"google.golang.org/grpc/experimental/balancer/weight"
3030
"google.golang.org/grpc/internal/grpctest"
3131
iringhash "google.golang.org/grpc/internal/ringhash"
3232
"google.golang.org/grpc/internal/testutils"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
*
3+
* Copyright 2026 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 hostname contains utilities for the endpoint hostname attribute
20+
// (used for per-endpoint :authority / SNI override).
21+
//
22+
// # Experimental
23+
//
24+
// Notice: All APIs in this package are EXPERIMENTAL and may be changed
25+
// or removed in a later release.
26+
package hostname
27+
28+
import "google.golang.org/grpc/resolver"
29+
30+
type hostnameKey struct{}
31+
32+
// Set returns a copy of the given endpoint with the hostname attribute
33+
// set. If hostname is empty the endpoint is returned unmodified.
34+
func Set(endpoint resolver.Endpoint, hostname string) resolver.Endpoint {
35+
if hostname == "" {
36+
return endpoint
37+
}
38+
endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKey{}, hostname)
39+
return endpoint
40+
}
41+
42+
// FromEndpoint returns the hostname attribute of endpoint. If this
43+
// attribute is not set, it returns the empty string.
44+
func FromEndpoint(endpoint resolver.Endpoint) string {
45+
h, _ := endpoint.Attributes.Value(hostnameKey{}).(string)
46+
return h
47+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
*
3+
* Copyright 2026 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 hostname_test
20+
21+
import (
22+
"testing"
23+
24+
"google.golang.org/grpc/attributes"
25+
"google.golang.org/grpc/experimental/balancer/hostname"
26+
"google.golang.org/grpc/internal/grpctest"
27+
"google.golang.org/grpc/resolver"
28+
)
29+
30+
type s struct {
31+
grpctest.Tester
32+
}
33+
34+
func Test(t *testing.T) {
35+
grpctest.RunSubTests(t, s{})
36+
}
37+
38+
func (s) TestHostnameToAndFromEndpoint(t *testing.T) {
39+
tests := []struct {
40+
desc string
41+
inputHostname string
42+
inputAttributes *attributes.Attributes
43+
wantHostname string
44+
}{
45+
{
46+
desc: "empty_attributes",
47+
inputHostname: "myservice.example.com",
48+
wantHostname: "myservice.example.com",
49+
},
50+
{
51+
desc: "non-empty_attributes",
52+
inputHostname: "myservice.example.com",
53+
inputAttributes: attributes.New("foo", "bar"),
54+
wantHostname: "myservice.example.com",
55+
},
56+
{
57+
desc: "hostname_not_present_in_empty_attributes",
58+
},
59+
{
60+
desc: "hostname_not_present_in_non-empty_attributes",
61+
inputAttributes: attributes.New("foo", "bar"),
62+
},
63+
}
64+
65+
for _, test := range tests {
66+
t.Run(test.desc, func(t *testing.T) {
67+
endpoint := resolver.Endpoint{Attributes: test.inputAttributes}
68+
endpoint = hostname.Set(endpoint, test.inputHostname)
69+
gotHostname := hostname.FromEndpoint(endpoint)
70+
if gotHostname != test.wantHostname {
71+
t.Errorf("hostname.FromEndpoint(%+v) = %q, want %q", endpoint, gotHostname, test.wantHostname)
72+
}
73+
})
74+
}
75+
}
Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,23 @@
1616
*
1717
*/
1818

19-
// Package weight contains utilities to manage endpoint weights. Weights are
20-
// used by LB policies such as ringhash to distribute load across multiple
21-
// endpoints.
19+
// Package weight contains utilities to manage endpoint weights.
20+
// Weights may be used by LB policies to distribute load across
21+
// multiple endpoints.
22+
//
23+
// # Experimental
24+
//
25+
// Notice: All APIs in this package are EXPERIMENTAL and may be changed
26+
// or removed in a later release.
2227
package weight
2328

24-
import (
25-
"fmt"
26-
27-
"google.golang.org/grpc/resolver"
28-
)
29+
import "google.golang.org/grpc/resolver"
2930

3031
// attributeKey is the type used as the key to store EndpointInfo in the
3132
// Attributes field of resolver.Endpoint.
3233
type attributeKey struct{}
3334

34-
// EndpointInfo will be stored in the Attributes field of Endpoints in order to
35-
// use the ringhash balancer.
35+
// EndpointInfo will be stored in the Attributes field of Endpoints.
3636
type EndpointInfo struct {
3737
Weight uint32
3838
}
@@ -43,22 +43,16 @@ func (a EndpointInfo) Equal(o any) bool {
4343
return ok && oa.Weight == a.Weight
4444
}
4545

46-
// Set returns a copy of endpoint in which the Attributes field is updated with
47-
// EndpointInfo.
46+
// Set returns a copy of endpoint in which the Attributes field is
47+
// updated with EndpointInfo.
4848
func Set(endpoint resolver.Endpoint, epInfo EndpointInfo) resolver.Endpoint {
4949
endpoint.Attributes = endpoint.Attributes.WithValue(attributeKey{}, epInfo)
5050
return endpoint
5151
}
5252

53-
// String returns a human-readable representation of EndpointInfo.
54-
// This method is intended for logging, testing, and debugging purposes only.
55-
// Do not rely on the output format, as it is not guaranteed to remain stable.
56-
func (a EndpointInfo) String() string {
57-
return fmt.Sprintf("Weight: %d", a.Weight)
58-
}
59-
60-
// FromEndpoint returns the EndpointInfo stored in the Attributes field of an
61-
// endpoint. It returns an empty EndpointInfo if attribute is not found.
53+
// FromEndpoint returns the EndpointInfo stored in the Attributes
54+
// field of an endpoint. It returns an empty EndpointInfo if attribute
55+
// is not found.
6256
func FromEndpoint(endpoint resolver.Endpoint) EndpointInfo {
6357
v := endpoint.Attributes.Value(attributeKey{})
6458
ei, _ := v.(EndpointInfo)

internal/balancer/weight/weight_test.go renamed to experimental/balancer/weight/weight_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323

2424
"github.com/google/go-cmp/cmp"
2525
"google.golang.org/grpc/attributes"
26-
"google.golang.org/grpc/internal/balancer/weight"
26+
"google.golang.org/grpc/experimental/balancer/weight"
2727
"google.golang.org/grpc/internal/grpctest"
2828
"google.golang.org/grpc/resolver"
2929
)

‎internal/xds/balancer/cdsbalancer/configbuilder.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import (
2424
"maps"
2525
"slices"
2626

27-
"google.golang.org/grpc/internal/balancer/weight"
27+
"google.golang.org/grpc/experimental/balancer/hostname"
28+
"google.golang.org/grpc/experimental/balancer/weight"
2829
"google.golang.org/grpc/internal/envconfig"
2930
"google.golang.org/grpc/internal/hierarchy"
3031
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
@@ -181,7 +182,7 @@ func buildClusterImplConfigForDNS(g *nameGenerator, config *xdsresource.ClusterC
181182
// LB policies that rely on locality information (like weighted_target)
182183
// continue to work.
183184
localityStr := xdsinternal.LocalityString(clients.Locality{})
184-
retEndpoint = xdsresource.SetHostname(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
185+
retEndpoint = hostname.Set(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
185186
// Set the locality weight to 1. This is required because the child policy
186187
// like weighted_target which relies on locality weights to distribute
187188
// traffic. These policies may drop traffic if the weight is 0.

0 commit comments

Comments
 (0)