Repository: grpc/grpc-go

Issue:
Consider moving ring hash out of the xds package
The implementation of gRFC A76 (https://github.com/grpc/grpc-go/pull/8159) due in 1.72.0 makes it possible to use the ring hash balancer without xds. Most of the implementation currently lies in the `xds/internal/balancer/ringhash` package.

We should move it somewhere where it can be imported without pulling in all of xds.

Hints from issue comments:
@atollena: Waiting on https://github.com/grpc/grpc-go/pull/8159/files to be merged.

Solution patch:
diff --git a/.github/workflows/deps.yml b/.github/workflows/deps.yml
index 17b05eb1..a0457ece 100644
--- a/.github/workflows/deps.yml
+++ b/.github/workflows/deps.yml
@@ -42,8 +42,19 @@ jobs:
           mkdir "${TEMP_DIR}/before"
           scripts/gen-deps.sh "${TEMP_DIR}/before"
 
-          echo "Comparing dependencies..."
+          echo -e " \nComparing dependencies..."
           cd "${TEMP_DIR}"
-          # Run grep in a sub-shell since bash does not support ! in the middle of a pipe
-          diff -u0 -r "before" "after" | bash -c '! grep -v "@@"'
-          echo "No changes detected."
+          # Run grep in a sub-shell since bash does not support ! in the middle of a pipe.
+          if diff -u0 -r "before" "after" | bash -c '! grep -v "@@"'; then
+            echo "No changes detected."
+            exit 0
+          fi
+
+          # Print packages in `after` but not `before`.
+          for x in $(ls -1 after | grep -vF "$(ls -1 before)"); do
+            echo -e " \nDependencies of new package $x:"
+            cat "after/$x"
+          done
+
+          echo -e " \nChanges detected; exiting with error."
+          exit 1
diff --git a/xds/internal/balancer/ringhash/config.go b/balancer/ringhash/config.go
similarity index 85%
rename from xds/internal/balancer/ringhash/config.go
rename to balancer/ringhash/config.go
index 044d698a..65d9e8e4 100644
--- a/xds/internal/balancer/ringhash/config.go
+++ b/balancer/ringhash/config.go
@@ -25,26 +25,17 @@ import (
 
 	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/metadata"
-	"google.golang.org/grpc/serviceconfig"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 )
 
-// LBConfig is the balancer config for ring_hash balancer.
-type LBConfig struct {
-	serviceconfig.LoadBalancingConfig `json:"-"`
-
-	MinRingSize       uint64 `json:"minRingSize,omitempty"`
-	MaxRingSize       uint64 `json:"maxRingSize,omitempty"`
-	RequestHashHeader string `json:"requestHashHeader,omitempty"`
-}
-
 const (
 	defaultMinSize         = 1024
 	defaultMaxSize         = 4096
 	ringHashSizeUpperBound = 8 * 1024 * 1024 // 8M
 )
 
-func parseConfig(c json.RawMessage) (*LBConfig, error) {
-	var cfg LBConfig
+func parseConfig(c json.RawMessage) (*iringhash.LBConfig, error) {
+	var cfg iringhash.LBConfig
 	if err := json.Unmarshal(c, &cfg); err != nil {
 		return nil, err
 	}
diff --git a/xds/internal/balancer/ringhash/logging.go b/balancer/ringhash/logging.go
similarity index 100%
rename from xds/internal/balancer/ringhash/logging.go
rename to balancer/ringhash/logging.go
diff --git a/xds/internal/balancer/ringhash/picker.go b/balancer/ringhash/picker.go
similarity index 97%
rename from xds/internal/balancer/ringhash/picker.go
rename to balancer/ringhash/picker.go
index 1488d780..96f72905 100644
--- a/xds/internal/balancer/ringhash/picker.go
+++ b/balancer/ringhash/picker.go
@@ -23,8 +23,10 @@ import (
 	"strings"
 
 	xxhash "github.com/cespare/xxhash/v2"
+
 	"google.golang.org/grpc/balancer"
 	"google.golang.org/grpc/connectivity"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	"google.golang.org/grpc/metadata"
 )
 
@@ -55,7 +57,7 @@ func (p *picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
 	var requestHash uint64
 	if p.requestHashHeader == "" {
 		var ok bool
-		if requestHash, ok = XDSRequestHash(info.Ctx); !ok {
+		if requestHash, ok = iringhash.XDSRequestHash(info.Ctx); !ok {
 			return balancer.PickResult{}, fmt.Errorf("ringhash: expected xDS config selector to set the request hash")
 		}
 	} else {
diff --git a/xds/internal/balancer/ringhash/ring.go b/balancer/ringhash/ring.go
similarity index 100%
rename from xds/internal/balancer/ringhash/ring.go
rename to balancer/ringhash/ring.go
diff --git a/xds/internal/balancer/ringhash/ringhash.go b/balancer/ringhash/ringhash.go
similarity index 96%
rename from xds/internal/balancer/ringhash/ringhash.go
rename to balancer/ringhash/ringhash.go
index 87d26db3..8f20410e 100644
--- a/xds/internal/balancer/ringhash/ringhash.go
+++ b/balancer/ringhash/ringhash.go
@@ -16,7 +16,11 @@
  *
  */
 
-// Package ringhash implements the ringhash balancer.
+// Package ringhash implements the ringhash balancer. See the following
+// gRFCs for details:
+// - https://github.com/grpc/proposal/blob/master/A42-xds-ring-hash-lb-policy.md
+// - https://github.com/grpc/proposal/blob/master/A61-IPv4-IPv6-dualstack-backends.md#ring-hash
+// - https://github.com/grpc/proposal/blob/master/A76-ring-hash-improvements.md
 package ringhash
 
 import (
@@ -36,6 +40,7 @@ import (
 	"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"
 	"google.golang.org/grpc/resolver"
 	"google.golang.org/grpc/resolver/ringhash"
 	"google.golang.org/grpc/serviceconfig"
@@ -85,7 +90,7 @@ type ringhashBalancer struct {
 	child  balancer.Balancer
 
 	mu                   sync.Mutex
-	config               *LBConfig
+	config               *iringhash.LBConfig
 	inhibitChildUpdates  bool
 	shouldRegenerateRing bool
 	endpointStates       *resolver.EndpointMap[*endpointState]
@@ -173,7 +178,7 @@ func (b *ringhashBalancer) UpdateClientConnState(ccs balancer.ClientConnState) e
 		b.logger.Infof("Received update from resolver, balancer config: %+v", pretty.ToJSON(ccs.BalancerConfig))
 	}
 
-	newConfig, ok := ccs.BalancerConfig.(*LBConfig)
+	newConfig, ok := ccs.BalancerConfig.(*iringhash.LBConfig)
 	if !ok {
 		return fmt.Errorf("unexpected balancer config with type: %T", ccs.BalancerConfig)
 	}
diff --git a/xds/internal/balancer/ringhash/util.go b/internal/ringhash/ringhash.go
similarity index 64%
rename from xds/internal/balancer/ringhash/util.go
rename to internal/ringhash/ringhash.go
index 371c3c3e..c75ac1ce 100644
--- a/xds/internal/balancer/ringhash/util.go
+++ b/internal/ringhash/ringhash.go
@@ -1,6 +1,6 @@
 /*
  *
- * Copyright 2021 gRPC authors.
+ * Copyright 2025 gRPC authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,12 +16,28 @@
  *
  */
 
+// Package ringhash (internal) contains functions and types that need to be
+// shared by the ring hash balancer and other gRPC code (such as xDS)
+// without being exported.
 package ringhash
 
 import (
 	"context"
+
+	"google.golang.org/grpc/serviceconfig"
 )
 
+// LBConfig is the balancer config for ring_hash balancer.
+type LBConfig struct {
+	serviceconfig.LoadBalancingConfig `json:"-"`
+
+	MinRingSize       uint64 `json:"minRingSize,omitempty"`
+	MaxRingSize       uint64 `json:"maxRingSize,omitempty"`
+	RequestHashHeader string `json:"requestHashHeader,omitempty"`
+}
+
+// xdsHashKey is the type used as the key to store request hash in the context
+// used when combining the Ring Hash load balancing policy with xDS.
 type xdsHashKey struct{}
 
 // XDSRequestHash returns the request hash in the context and true if it was set
diff --git a/xds/internal/resolver/serviceconfig.go b/xds/internal/resolver/serviceconfig.go
index f8c8a965..40f038b8 100644
--- a/xds/internal/resolver/serviceconfig.go
+++ b/xds/internal/resolver/serviceconfig.go
@@ -32,12 +32,12 @@ import (
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/internal/grpcutil"
 	iresolver "google.golang.org/grpc/internal/resolver"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	"google.golang.org/grpc/internal/serviceconfig"
 	"google.golang.org/grpc/internal/wrr"
 	"google.golang.org/grpc/metadata"
 	"google.golang.org/grpc/status"
 	"google.golang.org/grpc/xds/internal/balancer/clustermanager"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 	"google.golang.org/grpc/xds/internal/httpfilter"
 	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
 )
@@ -142,7 +142,7 @@ type erroringConfigSelector struct {
 }
 
 func newErroringConfigSelector(err error, xdsNodeID string) *erroringConfigSelector {
-	return &erroringConfigSelector{err: annotateErrorWithNodeID(status.Errorf(codes.Unavailable, err.Error()), xdsNodeID)}
+	return &erroringConfigSelector{err: annotateErrorWithNodeID(status.Error(codes.Unavailable, err.Error()), xdsNodeID)}
 }
 
 func (cs *erroringConfigSelector) SelectConfig(iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
@@ -203,7 +203,7 @@ func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RP
 	}
 
 	lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name)
-	lbCtx = ringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
+	lbCtx = iringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
 
 	config := &iresolver.RPCConfig{
 		// Communicate to the LB policy the chosen cluster and request hash, if Ring Hash LB policy.
diff --git a/xds/internal/xdsclient/xdslbregistry/converter/converter.go b/xds/internal/xdsclient/xdslbregistry/converter/converter.go
index 3c48f1bd..f0d064c6 100644
--- a/xds/internal/xdsclient/xdslbregistry/converter/converter.go
+++ b/xds/internal/xdsclient/xdslbregistry/converter/converter.go
@@ -30,11 +30,12 @@ import (
 	"google.golang.org/grpc/balancer"
 	"google.golang.org/grpc/balancer/leastrequest"
 	"google.golang.org/grpc/balancer/pickfirst"
+	"google.golang.org/grpc/balancer/ringhash"
 	"google.golang.org/grpc/balancer/roundrobin"
 	"google.golang.org/grpc/balancer/weightedroundrobin"
 	"google.golang.org/grpc/internal/envconfig"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 	"google.golang.org/grpc/xds/internal/balancer/wrrlocality"
 	"google.golang.org/grpc/xds/internal/xdsclient/xdslbregistry"
 	"google.golang.org/protobuf/proto"
@@ -83,7 +84,7 @@ func convertRingHashProtoToServiceConfig(rawProto []byte, _ int) (json.RawMessag
 		maxSize = max.GetValue()
 	}
 
-	rhCfg := &ringhash.LBConfig{
+	rhCfg := &iringhash.LBConfig{
 		MinRingSize: minSize,
 		MaxRingSize: maxSize,
 	}



Test patch:
diff --git a/xds/internal/balancer/ringhash/config_test.go b/balancer/ringhash/config_test.go
similarity index 86%
rename from xds/internal/balancer/ringhash/config_test.go
rename to balancer/ringhash/config_test.go
index b3772e45..20301482 100644
--- a/xds/internal/balancer/ringhash/config_test.go
+++ b/balancer/ringhash/config_test.go
@@ -23,7 +23,9 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+
 	"google.golang.org/grpc/internal/envconfig"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	"google.golang.org/grpc/internal/testutils"
 )
 
@@ -33,26 +35,26 @@ func (s) TestParseConfig(t *testing.T) {
 		js                  string
 		envConfigCap        uint64
 		requestHeaderEnvVar bool
-		want                *LBConfig
+		want                *iringhash.LBConfig
 		wantErr             bool
 	}{
 		{
 			name:                "OK",
 			js:                  `{"minRingSize": 1, "maxRingSize": 2}`,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: 1, MaxRingSize: 2},
+			want:                &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 2},
 		},
 		{
 			name:                "OK with default min",
 			js:                  `{"maxRingSize": 2000}`,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: defaultMinSize, MaxRingSize: 2000},
+			want:                &iringhash.LBConfig{MinRingSize: defaultMinSize, MaxRingSize: 2000},
 		},
 		{
 			name:                "OK with default max",
 			js:                  `{"minRingSize": 2000}`,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: 2000, MaxRingSize: defaultMaxSize},
+			want:                &iringhash.LBConfig{MinRingSize: 2000, MaxRingSize: defaultMaxSize},
 		},
 		{
 			name:                "min greater than max",
@@ -72,27 +74,27 @@ func (s) TestParseConfig(t *testing.T) {
 			name:                "max greater than global limit",
 			js:                  `{"minRingSize": 1, "maxRingSize": 6000}`,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: 1, MaxRingSize: 4096},
+			want:                &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 4096},
 		},
 		{
 			name:                "min and max greater than global limit",
 			js:                  `{"minRingSize": 5000, "maxRingSize": 6000}`,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: 4096, MaxRingSize: 4096},
+			want:                &iringhash.LBConfig{MinRingSize: 4096, MaxRingSize: 4096},
 		},
 		{
 			name:                "min and max less than raised global limit",
 			js:                  `{"minRingSize": 5000, "maxRingSize": 6000}`,
 			envConfigCap:        8000,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: 5000, MaxRingSize: 6000},
+			want:                &iringhash.LBConfig{MinRingSize: 5000, MaxRingSize: 6000},
 		},
 		{
 			name:                "min and max greater than raised global limit",
 			js:                  `{"minRingSize": 10000, "maxRingSize": 10000}`,
 			envConfigCap:        8000,
 			requestHeaderEnvVar: true,
-			want:                &LBConfig{MinRingSize: 8000, MaxRingSize: 8000},
+			want:                &iringhash.LBConfig{MinRingSize: 8000, MaxRingSize: 8000},
 		},
 		{
 			name:                "min greater than upper bound",
@@ -112,7 +114,7 @@ func (s) TestParseConfig(t *testing.T) {
 			name:                "request metadata key set",
 			js:                  `{"requestHashHeader": "x-foo"}`,
 			requestHeaderEnvVar: true,
-			want: &LBConfig{
+			want: &iringhash.LBConfig{
 				MinRingSize:       defaultMinSize,
 				MaxRingSize:       defaultMaxSize,
 				RequestHashHeader: "x-foo",
@@ -122,7 +124,7 @@ func (s) TestParseConfig(t *testing.T) {
 			name:                "request metadata key set with uppercase letters",
 			js:                  `{"requestHashHeader": "x-FOO"}`,
 			requestHeaderEnvVar: true,
-			want: &LBConfig{
+			want: &iringhash.LBConfig{
 				MinRingSize:       defaultMinSize,
 				MaxRingSize:       defaultMaxSize,
 				RequestHashHeader: "x-foo",
@@ -146,7 +148,7 @@ func (s) TestParseConfig(t *testing.T) {
 			name:                "request hash header cleared when RingHashSetRequestHashKey env var is false",
 			js:                  `{"requestHashHeader": "x-foo"}`,
 			requestHeaderEnvVar: false,
-			want: &LBConfig{
+			want: &iringhash.LBConfig{
 				MinRingSize: defaultMinSize,
 				MaxRingSize: defaultMaxSize,
 			},
diff --git a/xds/internal/balancer/ringhash/picker_test.go b/balancer/ringhash/picker_test.go
similarity index 98%
rename from xds/internal/balancer/ringhash/picker_test.go
rename to balancer/ringhash/picker_test.go
index 2c079c19..88352643 100644
--- a/xds/internal/balancer/ringhash/picker_test.go
+++ b/balancer/ringhash/picker_test.go
@@ -28,6 +28,7 @@ import (
 
 	"google.golang.org/grpc/balancer"
 	"google.golang.org/grpc/connectivity"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	"google.golang.org/grpc/internal/testutils"
 	"google.golang.org/grpc/metadata"
 )
@@ -156,7 +157,7 @@ func (s) TestPickerPickFirstTwo(t *testing.T) {
 				endpointStates: epStates,
 			}
 			got, err := p.Pick(balancer.PickInfo{
-				Ctx: SetXDSRequestHash(ctx, 0), // always pick the first endpoint on the ring.
+				Ctx: iringhash.SetXDSRequestHash(ctx, 0), // always pick the first endpoint on the ring.
 			})
 			if (err != nil || tt.wantErr != nil) && !errors.Is(err, tt.wantErr) {
 				t.Errorf("Pick() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/xds/internal/balancer/ringhash/ring_test.go b/balancer/ringhash/ring_test.go
similarity index 99%
rename from xds/internal/balancer/ringhash/ring_test.go
rename to balancer/ringhash/ring_test.go
index c069a7d7..01568155 100644
--- a/xds/internal/balancer/ringhash/ring_test.go
+++ b/balancer/ringhash/ring_test.go
@@ -23,10 +23,9 @@ import (
 	"math"
 	"testing"
 
+	xxhash "github.com/cespare/xxhash/v2"
 	"google.golang.org/grpc/internal/balancer/weight"
 	"google.golang.org/grpc/resolver"
-
-	xxhash "github.com/cespare/xxhash/v2"
 )
 
 var testEndpoints []resolver.Endpoint
diff --git a/xds/internal/balancer/ringhash/e2e/ringhash_balancer_test.go b/balancer/ringhash/ringhash_e2e_test.go
similarity index 99%
rename from xds/internal/balancer/ringhash/e2e/ringhash_balancer_test.go
rename to balancer/ringhash/ringhash_e2e_test.go
index 2a3e1a1b..2c6d7bff 100644
--- a/xds/internal/balancer/ringhash/e2e/ringhash_balancer_test.go
+++ b/balancer/ringhash/ringhash_e2e_test.go
@@ -42,6 +42,7 @@ import (
 	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/grpctest"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	"google.golang.org/grpc/internal/stubserver"
 	"google.golang.org/grpc/internal/testutils"
 	"google.golang.org/grpc/internal/testutils/xds/e2e"
@@ -50,7 +51,6 @@ import (
 	"google.golang.org/grpc/resolver"
 	"google.golang.org/grpc/resolver/manual"
 	"google.golang.org/grpc/status"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 
 	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
 	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
@@ -134,7 +134,7 @@ func (s) TestRingHash_ReconnectToMoveOutOfTransientFailure(t *testing.T) {
 	r.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: lis.Addr().String()}}})
 
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
-	ctx = ringhash.SetXDSRequestHash(ctx, 0)
+	ctx = iringhash.SetXDSRequestHash(ctx, 0)
 	defer cancel()
 	client := testgrpc.NewTestServiceClient(cc)
 	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
diff --git a/xds/internal/balancer/ringhash/ringhash_test.go b/balancer/ringhash/ringhash_test.go
similarity index 90%
rename from xds/internal/balancer/ringhash/ringhash_test.go
rename to balancer/ringhash/ringhash_test.go
index 8331c1b6..385ee0ad 100644
--- a/xds/internal/balancer/ringhash/ringhash_test.go
+++ b/balancer/ringhash/ringhash_test.go
@@ -28,9 +28,9 @@ import (
 	"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"
 	"google.golang.org/grpc/resolver"
-	"google.golang.org/grpc/xds/internal"
 )
 
 const (
@@ -42,7 +42,7 @@ const (
 
 var (
 	testBackendAddrStrs []string
-	testConfig          = &LBConfig{MinRingSize: 1, MaxRingSize: 10}
+	testConfig          = &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 10}
 )
 
 func init() {
@@ -114,8 +114,11 @@ func (s) TestUpdateClientConnState_NewRingSize(t *testing.T) {
 	}
 
 	if err := b.UpdateClientConnState(balancer.ClientConnState{
-		ResolverState:  resolver.State{Endpoints: endpoints},
-		BalancerConfig: &LBConfig{MinRingSize: uint64(newMinRingSize), MaxRingSize: uint64(newMaxRingSize)},
+		ResolverState: resolver.State{Endpoints: endpoints},
+		BalancerConfig: &iringhash.LBConfig{
+			MinRingSize: uint64(newMinRingSize),
+			MaxRingSize: uint64(newMaxRingSize),
+		},
 	}); err != nil {
 		t.Fatalf("UpdateClientConnState returned err: %v", err)
 	}
@@ -144,7 +147,7 @@ func (s) TestOneEndpoint(t *testing.T) {
 	// only Endpoint which has a single address.
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
-	if _, err := p0.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
+	if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
 		t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
 	}
 	var sc0 *testutils.TestSubConn
@@ -172,7 +175,7 @@ func (s) TestOneEndpoint(t *testing.T) {
 	// Test pick with one backend.
 	p1 := <-cc.NewPickerCh
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)})
+		gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
 		if gotSCSt.SubConn != sc0 {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc0)
 		}
@@ -205,7 +208,7 @@ func (s) TestThreeSubConnsAffinity(t *testing.T) {
 	// SubConn.
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
-	if _, err := p0.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
+	if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
 		t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
 	}
 
@@ -292,7 +295,7 @@ func (s) TestThreeSubConnsAffinity(t *testing.T) {
 	}
 	p1 := <-cc.NewPickerCh
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)})
+		gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
 		if gotSCSt.SubConn != subConns[0] {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, subConns[0])
 		}
@@ -305,7 +308,7 @@ func (s) TestThreeSubConnsAffinity(t *testing.T) {
 	subConns[2].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
 	p2 := <-cc.NewPickerCh
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)})
+		gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
 		if gotSCSt.SubConn != subConns[2] {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, subConns[2])
 		}
@@ -318,7 +321,7 @@ func (s) TestThreeSubConnsAffinity(t *testing.T) {
 	subConns[1].UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
 	p3 := <-cc.NewPickerCh
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p3.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)})
+		gotSCSt, _ := p3.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
 		if gotSCSt.SubConn != subConns[1] {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, subConns[1])
 		}
@@ -346,7 +349,7 @@ func (s) TestThreeBackendsAffinityMultiple(t *testing.T) {
 	// SubConn.
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
-	if _, err := p0.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
+	if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)}); err != balancer.ErrNoSubConnAvailable {
 		t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
 	}
 	// The picked SubConn should be the second in the ring.
@@ -375,7 +378,7 @@ func (s) TestThreeBackendsAffinityMultiple(t *testing.T) {
 	// First hash should always pick sc0.
 	p1 := <-cc.NewPickerCh
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)})
+		gotSCSt, _ := p1.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
 		if gotSCSt.SubConn != sc0 {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc0)
 		}
@@ -384,7 +387,7 @@ func (s) TestThreeBackendsAffinityMultiple(t *testing.T) {
 	secondHash := ring0.items[1].hash
 	// secondHash+1 will pick the third SubConn from the ring.
 	testHash2 := secondHash + 1
-	if _, err := p0.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash2)}); err != balancer.ErrNoSubConnAvailable {
+	if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash2)}); err != balancer.ErrNoSubConnAvailable {
 		t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
 	}
 	var sc1 *testutils.TestSubConn
@@ -407,14 +410,14 @@ func (s) TestThreeBackendsAffinityMultiple(t *testing.T) {
 	// With the new generated picker, hash2 always picks sc1.
 	p2 := <-cc.NewPickerCh
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash2)})
+		gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash2)})
 		if gotSCSt.SubConn != sc1 {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc1)
 		}
 	}
 	// But the first hash still picks sc0.
 	for i := 0; i < 5; i++ {
-		gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, testHash)})
+		gotSCSt, _ := p2.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, testHash)})
 		if gotSCSt.SubConn != sc0 {
 			t.Fatalf("picker.Pick, got %v, want SubConn=%v", gotSCSt, sc0)
 		}
@@ -532,7 +535,7 @@ func (s) TestAutoConnectEndpointOnTransientFailure(t *testing.T) {
 	// ringhash won't tell SCs to connect until there is an RPC, so simulate
 	// one now.
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
-	ctx = SetXDSRequestHash(ctx, 0)
+	ctx = iringhash.SetXDSRequestHash(ctx, 0)
 	defer cancel()
 	p0.Pick(balancer.PickInfo{Ctx: ctx})
 
@@ -672,16 +675,34 @@ func (s) TestAggregatedConnectivityState(t *testing.T) {
 	}
 }
 
+type testKeyType string
+
+const testKey testKeyType = "grpc.lb.ringhash.testKey"
+
+type testAttribute struct {
+	content string
+}
+
+func setTestAttrAddr(addr resolver.Address, content string) resolver.Address {
+	addr.BalancerAttributes = addr.BalancerAttributes.WithValue(testKey, testAttribute{content})
+	return addr
+}
+
+func setTestAttrEndpoint(endpoint resolver.Endpoint, content string) resolver.Endpoint {
+	endpoint.Attributes = endpoint.Attributes.WithValue(testKey, testAttribute{content})
+	return endpoint
+}
+
 // TestAddrBalancerAttributesChange tests the case where the ringhash balancer
 // receives a ClientConnUpdate with the same config and addresses as received in
 // the previous update. Although the `BalancerAttributes` and endpoint
 // attributes contents are the same, the pointers are different. This test
 // verifies that subConns are not recreated in this scenario.
 func (s) TestAddrBalancerAttributesChange(t *testing.T) {
-	locality := internal.LocalityID{Region: "americas"}
-	addrs1 := []resolver.Address{internal.SetLocalityID(resolver.Address{Addr: testBackendAddrStrs[0]}, locality)}
+	content := "test"
+	addrs1 := []resolver.Address{setTestAttrAddr(resolver.Address{Addr: testBackendAddrStrs[0]}, content)}
 	wantEndpoints1 := []resolver.Endpoint{
-		internal.SetLocalityIDInEndpoint(resolver.Endpoint{Addresses: addrs1}, locality),
+		setTestAttrEndpoint(resolver.Endpoint{Addresses: addrs1}, "content"),
 	}
 	cc, b, p0 := setupTest(t, wantEndpoints1)
 	ring0 := p0.(*picker).ring
@@ -691,7 +712,7 @@ func (s) TestAddrBalancerAttributesChange(t *testing.T) {
 	// only Endpoint which has a single address.
 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
 	defer cancel()
-	if _, err := p0.Pick(balancer.PickInfo{Ctx: SetXDSRequestHash(ctx, firstHash)}); err != balancer.ErrNoSubConnAvailable {
+	if _, err := p0.Pick(balancer.PickInfo{Ctx: iringhash.SetXDSRequestHash(ctx, firstHash)}); err != balancer.ErrNoSubConnAvailable {
 		t.Fatalf("first pick returned err %v, want %v", err, balancer.ErrNoSubConnAvailable)
 	}
 	select {
@@ -700,10 +721,8 @@ func (s) TestAddrBalancerAttributesChange(t *testing.T) {
 	case <-cc.NewSubConnCh:
 	}
 
-	addrs2 := []resolver.Address{internal.SetLocalityID(resolver.Address{Addr: testBackendAddrStrs[0]}, locality)}
-	wantEndpoints2 := []resolver.Endpoint{
-		internal.SetLocalityIDInEndpoint(resolver.Endpoint{Addresses: addrs2}, locality),
-	}
+	addrs2 := []resolver.Address{setTestAttrAddr(resolver.Address{Addr: testBackendAddrStrs[0]}, content)}
+	wantEndpoints2 := []resolver.Endpoint{setTestAttrEndpoint(resolver.Endpoint{Addresses: addrs2}, content)}
 	if err := b.UpdateClientConnState(balancer.ClientConnState{
 		ResolverState:  resolver.State{Endpoints: wantEndpoints2},
 		BalancerConfig: testConfig,
diff --git a/xds/internal/balancer/cdsbalancer/cdsbalancer_test.go b/xds/internal/balancer/cdsbalancer/cdsbalancer_test.go
index 78d8e118..87888c73 100644
--- a/xds/internal/balancer/cdsbalancer/cdsbalancer_test.go
+++ b/xds/internal/balancer/cdsbalancer/cdsbalancer_test.go
@@ -58,7 +58,7 @@ import (
 	testgrpc "google.golang.org/grpc/interop/grpc_testing"
 	testpb "google.golang.org/grpc/interop/grpc_testing"
 
-	_ "google.golang.org/grpc/xds/internal/balancer/ringhash" // Register the ring_hash LB policy
+	_ "google.golang.org/grpc/balancer/ringhash" // Register the ring_hash LB policy
 )
 
 const (
diff --git a/xds/internal/balancer/clusterresolver/config_test.go b/xds/internal/balancer/clusterresolver/config_test.go
index 30befd5b..79c135a6 100644
--- a/xds/internal/balancer/clusterresolver/config_test.go
+++ b/xds/internal/balancer/clusterresolver/config_test.go
@@ -26,11 +26,12 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"google.golang.org/grpc/balancer"
+	"google.golang.org/grpc/balancer/ringhash"
 	"google.golang.org/grpc/balancer/roundrobin"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
 	"google.golang.org/grpc/internal/xds/bootstrap"
 	"google.golang.org/grpc/xds/internal/balancer/outlierdetection"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 )
 
 func TestDiscoveryMechanismTypeMarshalJSON(t *testing.T) {
@@ -303,8 +304,12 @@ func TestParseConfig(t *testing.T) {
 					},
 				},
 				xdsLBPolicy: iserviceconfig.BalancerConfig{
-					Name:   ringhash.Name,
-					Config: &ringhash.LBConfig{MinRingSize: 1024, MaxRingSize: 4096}, // Ringhash LB config with default min and max.
+					Name: ringhash.Name,
+					Config: &iringhash.LBConfig{
+						// Ringhash LB config with default min and max.
+						MinRingSize: 1024,
+						MaxRingSize: 4096,
+					},
 				},
 			},
 			wantErr: false,
diff --git a/xds/internal/balancer/clusterresolver/configbuilder_test.go b/xds/internal/balancer/clusterresolver/configbuilder_test.go
index 8c856fd1..a1d5bc85 100644
--- a/xds/internal/balancer/clusterresolver/configbuilder_test.go
+++ b/xds/internal/balancer/clusterresolver/configbuilder_test.go
@@ -29,9 +29,11 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"google.golang.org/grpc/attributes"
 	"google.golang.org/grpc/balancer"
+	"google.golang.org/grpc/balancer/ringhash"
 	"google.golang.org/grpc/balancer/roundrobin"
 	"google.golang.org/grpc/internal/balancer/weight"
 	"google.golang.org/grpc/internal/hierarchy"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
 	"google.golang.org/grpc/internal/xds/bootstrap"
 	"google.golang.org/grpc/resolver"
@@ -39,7 +41,6 @@ import (
 	"google.golang.org/grpc/xds/internal/balancer/clusterimpl"
 	"google.golang.org/grpc/xds/internal/balancer/outlierdetection"
 	"google.golang.org/grpc/xds/internal/balancer/priority"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 	"google.golang.org/grpc/xds/internal/balancer/wrrlocality"
 	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
 )
@@ -601,12 +602,12 @@ func TestPriorityLocalitiesToClusterImpl(t *testing.T) {
 				},
 			},
 			priorityName: "test-priority",
-			childPolicy:  &iserviceconfig.BalancerConfig{Name: ringhash.Name, Config: &ringhash.LBConfig{MinRingSize: 1, MaxRingSize: 2}},
+			childPolicy:  &iserviceconfig.BalancerConfig{Name: ringhash.Name, Config: &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 2}},
 			// lrsServer is nil, so LRS policy will not be used.
 			wantConfig: &clusterimpl.LBConfig{
 				ChildPolicy: &iserviceconfig.BalancerConfig{
 					Name:   ringhash.Name,
-					Config: &ringhash.LBConfig{MinRingSize: 1, MaxRingSize: 2},
+					Config: &iringhash.LBConfig{MinRingSize: 1, MaxRingSize: 2},
 				},
 			},
 			wantEndpoints: []resolver.Endpoint{
diff --git a/xds/internal/resolver/xds_resolver_test.go b/xds/internal/resolver/xds_resolver_test.go
index 641fc584..1821fe18 100644
--- a/xds/internal/resolver/xds_resolver_test.go
+++ b/xds/internal/resolver/xds_resolver_test.go
@@ -35,6 +35,7 @@ import (
 	estats "google.golang.org/grpc/experimental/stats"
 	"google.golang.org/grpc/internal"
 	iresolver "google.golang.org/grpc/internal/resolver"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	"google.golang.org/grpc/internal/testutils"
 	"google.golang.org/grpc/internal/testutils/xds/e2e"
 	"google.golang.org/grpc/internal/xds/bootstrap"
@@ -42,7 +43,6 @@ import (
 	"google.golang.org/grpc/resolver"
 	"google.golang.org/grpc/serviceconfig"
 	"google.golang.org/grpc/xds/internal/balancer/clustermanager"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 	"google.golang.org/grpc/xds/internal/httpfilter"
 	rinternal "google.golang.org/grpc/xds/internal/resolver/internal"
 	"google.golang.org/grpc/xds/internal/xdsclient"
@@ -543,7 +543,7 @@ func (s) TestResolverRequestHash(t *testing.T) {
 		t.Fatalf("cs.SelectConfig(): %v", err)
 	}
 	wantHash := xxhash.Sum64String("/products")
-	gotHash, ok := ringhash.XDSRequestHash(res.Context)
+	gotHash, ok := iringhash.XDSRequestHash(res.Context)
 	if !ok {
 		t.Fatalf("Got no request hash, want: %v", wantHash)
 	}
diff --git a/xds/internal/xdsclient/xdsresource/tests/unmarshal_cds_test.go b/xds/internal/xdsclient/xdsresource/tests/unmarshal_cds_test.go
index 75ca714c..f122b372 100644
--- a/xds/internal/xdsclient/xdsresource/tests/unmarshal_cds_test.go
+++ b/xds/internal/xdsclient/xdsresource/tests/unmarshal_cds_test.go
@@ -29,13 +29,13 @@ import (
 	"google.golang.org/grpc/internal/balancer/stub"
 	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/grpctest"
+	iringhash "google.golang.org/grpc/internal/ringhash"
 	iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
 	"google.golang.org/grpc/internal/testutils"
 	"google.golang.org/grpc/internal/testutils/xds/e2e"
 	"google.golang.org/grpc/internal/xds/bootstrap"
 	"google.golang.org/grpc/serviceconfig"
 	"google.golang.org/grpc/xds/internal"
-	"google.golang.org/grpc/xds/internal/balancer/ringhash"
 	"google.golang.org/grpc/xds/internal/balancer/wrrlocality"
 	"google.golang.org/grpc/xds/internal/xdsclient/xdsresource"
 	"google.golang.org/protobuf/proto"
@@ -294,7 +294,7 @@ func (s) TestValidateCluster_Success(t *testing.T) {
 			},
 			wantLBConfig: &iserviceconfig.BalancerConfig{
 				Name: "ring_hash_experimental",
-				Config: &ringhash.LBConfig{
+				Config: &iringhash.LBConfig{
 					MinRingSize: 1024,
 					MaxRingSize: 4096,
 				},
@@ -347,7 +347,7 @@ func (s) TestValidateCluster_Success(t *testing.T) {
 			},
 			wantLBConfig: &iserviceconfig.BalancerConfig{
 				Name: "ring_hash_experimental",
-				Config: &ringhash.LBConfig{
+				Config: &iringhash.LBConfig{
 					MinRingSize: 10,
 					MaxRingSize: 100,
 				},
@@ -419,7 +419,7 @@ func (s) TestValidateCluster_Success(t *testing.T) {
 			},
 			wantLBConfig: &iserviceconfig.BalancerConfig{
 				Name: "ring_hash_experimental",
-				Config: &ringhash.LBConfig{
+				Config: &iringhash.LBConfig{
 					MinRingSize: 10,
 					MaxRingSize: 100,
 				},
@@ -544,7 +544,7 @@ func (s) TestValidateCluster_Success(t *testing.T) {
 			},
 			wantLBConfig: &iserviceconfig.BalancerConfig{
 				Name: "ring_hash_experimental",
-				Config: &ringhash.LBConfig{
+				Config: &iringhash.LBConfig{
 					MinRingSize: 10,
 					MaxRingSize: 100,
 				},
diff --git a/xds/server_test.go b/xds/server_test.go
index 205f2646..13d01a7b 100644
--- a/xds/server_test.go
+++ b/xds/server_test.go
@@ -701,9 +701,8 @@ func (s) TestServeAndCloseDoNotRace(t *testing.T) {
 	// resource error notifications for the invalid listener resource leading
 	// to service mode change to "not serving" each time.
 	//
-	// Even if the the server is currently NOT_SERVING, in the case (where we
-	// are NOT_SERVING and the new mode is also NOT_SERVING), the update is not
-	// suppressed as:
+	// Even if the server is currently NOT_SERVING and the new mode is also
+	// NOT_SERVING, the update is not suppressed as:
 	//   1. the error may have change
 	//   2. it provides a timestamp of the last backoff attempt
 	noopModeChangeCallback := func(_ net.Addr, _ ServingModeChangeArgs) {}