Skip to content
Open
58 changes: 58 additions & 0 deletions balancer/hostname/hostname.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

// Package hostname contains utilities for the endpoint hostname attribute
// (used for per-endpoint :authority / SNI override).
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
package hostname

import "google.golang.org/grpc/resolver"

type hostnameKey struct{}

// Set returns a copy of the given endpoint with the hostname attribute set.
// If hostname is empty the endpoint is returned unmodified.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func Set(endpoint resolver.Endpoint, hostname string) resolver.Endpoint {
if hostname == "" {
return endpoint
}
endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKey{}, hostname)
return endpoint
}

// FromEndpoint returns the hostname attribute of endpoint. If this attribute is
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: wrap the comment in 80 columns.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// not set, it returns the empty string.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func FromEndpoint(endpoint resolver.Endpoint) string {
h, _ := endpoint.Attributes.Value(hostnameKey{}).(string)
return h
}

44 changes: 44 additions & 0 deletions balancer/hostname/hostname_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package hostname_test

import (
"testing"

"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/resolver"
)

func TestHostname_SetAndGet(t *testing.T) {
Comment thread
Pranjali-2501 marked this conversation as resolved.
Outdated
ep := resolver.Endpoint{}
if h := hostname.FromEndpoint(ep); h != "" {
t.Errorf("empty = %q", h)
}

ep2 := hostname.Set(ep, "myservice.example.com")
if h := hostname.FromEndpoint(ep2); h != "myservice.example.com" {
t.Errorf("got %q", h)
}

// empty hostname returns same endpoint
ep3 := hostname.Set(ep2, "")
if hostname.FromEndpoint(ep3) != "myservice.example.com" {
t.Error("empty should not overwrite")
}
}
2 changes: 1 addition & 1 deletion balancer/pickfirst/pickfirst.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ import (

"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/pickfirst/internal"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/connectivity"
expstats "google.golang.org/grpc/experimental/stats"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
internalgrpclog "google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/pretty"
Expand Down
2 changes: 1 addition & 1 deletion balancer/pickfirst/pickfirst_ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import (
"google.golang.org/grpc/balancer"
pfbalancer "google.golang.org/grpc/balancer/pickfirst"
pfinternal "google.golang.org/grpc/balancer/pickfirst/internal"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/balancer/stub"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/channelz"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpcsync"
Expand Down
3 changes: 2 additions & 1 deletion balancer/ringhash/ring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import (
"testing"

xxhash "github.com/cespare/xxhash/v2"
"google.golang.org/grpc/internal/balancer/weight"

"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/resolver"
)

Expand Down
2 changes: 1 addition & 1 deletion balancer/ringhash/ringhash.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import (
"google.golang.org/grpc/balancer/endpointsharding"
"google.golang.org/grpc/balancer/lazy"
"google.golang.org/grpc/balancer/pickfirst"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/pretty"
iringhash "google.golang.org/grpc/internal/ringhash"
Expand Down
2 changes: 1 addition & 1 deletion balancer/ringhash/ringhash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
"time"

"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/grpctest"
iringhash "google.golang.org/grpc/internal/ringhash"
"google.golang.org/grpc/internal/testutils"
Expand Down
15 changes: 15 additions & 0 deletions internal/balancer/weight/weight.go → balancer/weight/weight.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
// Package weight contains utilities to manage endpoint weights. Weights are
// used by LB policies such as ringhash to distribute load across multiple
// endpoints.
//
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of putting a deprecated message and creating new files in balancer/weight/, use git move to move the files from internal/balancer/weight/weight.go to balancer/weight/weight.go.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done - i also added the "Experimental" comment because this is a newly-public API. is that fine?

// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
package weight

import (
Expand All @@ -45,6 +50,11 @@ func (a EndpointInfo) Equal(o any) bool {

// Set returns a copy of endpoint in which the Attributes field is updated with
// EndpointInfo.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func Set(endpoint resolver.Endpoint, epInfo EndpointInfo) resolver.Endpoint {
endpoint.Attributes = endpoint.Attributes.WithValue(attributeKey{}, epInfo)
return endpoint
Expand All @@ -59,6 +69,11 @@ func (a EndpointInfo) String() string {

// FromEndpoint returns the EndpointInfo stored in the Attributes field of an
// endpoint. It returns an empty EndpointInfo if attribute is not found.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func FromEndpoint(endpoint resolver.Endpoint) EndpointInfo {
v := endpoint.Attributes.Value(attributeKey{})
ei, _ := v.(EndpointInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/resolver"
)
Expand Down
5 changes: 3 additions & 2 deletions internal/xds/balancer/cdsbalancer/configbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import (
"maps"
"slices"

"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/hierarchy"
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
Expand Down Expand Up @@ -181,7 +182,7 @@ func buildClusterImplConfigForDNS(g *nameGenerator, config *xdsresource.ClusterC
// LB policies that rely on locality information (like weighted_target)
// continue to work.
localityStr := xdsinternal.LocalityString(clients.Locality{})
retEndpoint = xdsresource.SetHostname(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
retEndpoint = hostname.Set(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
// Set the locality weight to 1. This is required because the child policy
// like weighted_target which relies on locality weights to distribute
// traffic. These policies may drop traffic if the weight is 0.
Expand Down
2 changes: 1 addition & 1 deletion internal/xds/balancer/cdsbalancer/configbuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"google.golang.org/grpc/balancer/pickfirst"
"google.golang.org/grpc/balancer/ringhash"
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/hierarchy"
iringhash "google.golang.org/grpc/internal/ringhash"
Expand Down
32 changes: 9 additions & 23 deletions internal/xds/xdsclient/xdsresource/unmarshal_eds.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,23 @@ import (
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"

Comment thread
Pranjali-2501 marked this conversation as resolved.
Outdated
"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/pretty"
xdsinternal "google.golang.org/grpc/internal/xds"
"google.golang.org/grpc/internal/xds/clients"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/ringhash"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)

// hostnameKeyType is the key to store the hostname attribute in
// a resolver.Endpoint.
type hostnameKeyType struct{}

// SetHostname returns a copy of the given endpoint with hostname added
// as an attribute.
func SetHostname(endpoint resolver.Endpoint, hostname string) resolver.Endpoint {
// Only set if non-empty; xds_cluster_impl uses this to trigger :authority
// rewriting.
if hostname == "" {
return endpoint
}
endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKeyType{}, hostname)
return endpoint
}

// Hostname returns the hostname from the BalancerAttributes of the given
// Address. If this attribute is not set, it returns the empty string.
// Hostname returns the hostname from the given legacy Address.
// If this attribute is not set, it returns the empty string.
func Hostname(addr resolver.Address) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert the changes made in this function. We have an exported function to set hostname in endpoint. And will keep the function internal to retrieve the hostname resolver.Address.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @Pranjali-2501 reverting this function causes two issues:

  1. The original hostnameKeyType has been removed above, so it wouldn't compile.
  2. Even if we bring hostnameKeyType back, the test TestAuthorityOverridingWithTLS/Authority_Rewrite_Mismatch fails — because EDS unmarshal now writes hostname via hostname.Set (which uses the hostnameKey defined in balancer/hostname/hostname.go), but the original Hostname(addr) would read with its own internal key type, so the hostname is never found.

The current implementation delegates to hostname.FromEndpoint so both setter and getter use the same key. All tests pass. Does this approach work, or would you prefer a different solution?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, the best way will be to move this function to balancer/hostname/hostname.go, export it and name it FromAddress.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

hostname, _ := addr.BalancerAttributes.Value(hostnameKeyType{}).(string)
return hostname
ep := resolver.Endpoint{Attributes: addr.BalancerAttributes}
return hostname.FromEndpoint(ep)
}

func unmarshalEndpointsResource(r *anypb.Any) (string, EndpointsUpdate, error) {
Expand Down Expand Up @@ -166,7 +152,7 @@ func parseEndpoints(lbEndpoints []*v3endpointpb.LbEndpoint, uniqueEndpointAddrs
}
}
endpoint := resolver.Endpoint{Addresses: address}
endpoint = SetHostname(endpoint, lbEndpoint.GetEndpoint().GetHostname())
endpoint = hostname.Set(endpoint, lbEndpoint.GetEndpoint().GetHostname())
endpoint = ringhash.SetHashKey(endpoint, hashKey)
endpoints = append(endpoints, Endpoint{
ResolverEndpoint: endpoint,
Expand Down
3 changes: 2 additions & 1 deletion internal/xds/xdsclient/xdsresource/unmarshal_eds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
hostnameattr "google.golang.org/grpc/balancer/hostname"
Comment thread
Pranjali-2501 marked this conversation as resolved.
Outdated
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/pretty"
"google.golang.org/grpc/internal/testutils"
Expand Down Expand Up @@ -68,7 +69,7 @@ func buildResolverEndpoint(addr []string, hostname string) resolver.Endpoint {
address = append(address, resolver.Address{Addr: a})
}
resolverEndpoint := resolver.Endpoint{Addresses: address}
resolverEndpoint = SetHostname(resolverEndpoint, hostname)
resolverEndpoint = hostnameattr.Set(resolverEndpoint, hostname)
return resolverEndpoint
}

Expand Down
Loading