This is an automated email from the ASF dual-hosted git repository.

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git


The following commit(s) were added to refs/heads/develop by this push:
     new 59ee0cbc4 Feat/reuse snapshot consistent hash (#960)
59ee0cbc4 is described below

commit 59ee0cbc4b2bd4ca7b80acd9dc507774d8b603f5
Author: wm_03 <[email protected]>
AuthorDate: Mon Jul 6 20:17:16 2026 +0800

    Feat/reuse snapshot consistent hash (#960)
    
    * fix(cluster): Reuse consistent hash only for unchanged snapshots
    
    * fix: remove redundant reuseHealthyConsistentHashFrom call in 
withEndpointHealthForIDs
---
 pkg/cluster/cluster.go                   |  83 ++++++++++++++-
 pkg/cluster/cluster_test.go              | 172 +++++++++++++++++++++++++++++++
 pkg/server/cluster_manager_bench_test.go |  51 ++++++++-
 3 files changed, 302 insertions(+), 4 deletions(-)

diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go
index e64025c47..c6c27a70f 100644
--- a/pkg/cluster/cluster.go
+++ b/pkg/cluster/cluster.go
@@ -197,6 +197,7 @@ type EndpointSnapshot struct {
        healthyByAddress     map[string]bool
        lbPolicy             model.LbPolicyType
        consistentHashOnce   sync.Once
+       consistentHashMu     sync.RWMutex
        consistentHash       model.LbConsistentHashView
        consistentHashConfig model.ConsistentHash
 }
@@ -248,6 +249,7 @@ func newEndpointSnapshot(config *model.ClusterConfig, 
previous *EndpointSnapshot
                healthy := endpointSnapshotHealth(snapshotEndpoint, address, 
previous, inheritRuntimeHealth)
                snapshot.addEndpoint(snapshotEndpoint, address, healthy)
        }
+       snapshot.reuseHealthyConsistentHashFrom(previous)
        return snapshot
 }
 
@@ -395,9 +397,36 @@ func (s *EndpointSnapshot) HealthyConsistentHash() 
model.LbConsistentHashView {
                return nil
        }
        s.consistentHashOnce.Do(s.rebuildConsistentHash)
+       return s.cachedHealthyConsistentHash()
+}
+
+func (s *EndpointSnapshot) cachedHealthyConsistentHash() 
model.LbConsistentHashView {
+       if s == nil {
+               return nil
+       }
+       s.consistentHashMu.RLock()
+       defer s.consistentHashMu.RUnlock()
        return s.consistentHash
 }
 
+func (s *EndpointSnapshot) seedHealthyConsistentHash(hash 
model.LbConsistentHashView) {
+       if s == nil || hash == nil {
+               return
+       }
+       s.consistentHashOnce.Do(func() {
+               s.storeHealthyConsistentHash(hash)
+       })
+}
+
+func (s *EndpointSnapshot) storeHealthyConsistentHash(hash 
model.LbConsistentHashView) {
+       if s == nil || hash == nil {
+               return
+       }
+       s.consistentHashMu.Lock()
+       defer s.consistentHashMu.Unlock()
+       s.consistentHash = hash
+}
+
 // PickHealthyEndpoint gives request-path selectors a read-only view of healthy
 // endpoints and clones only the selected endpoint before returning it. The 
pick
 // callback must not mutate or retain the endpoint view.
@@ -545,5 +574,57 @@ func (s *EndpointSnapshot) rebuildConsistentHash() {
        if !ok {
                return
        }
-       s.consistentHash = 
model.ReadOnlyConsistentHash(newConsistentHash(s.consistentHashConfig, 
s.healthy))
+       hash := 
model.ReadOnlyConsistentHash(newConsistentHash(s.consistentHashConfig, 
s.healthy))
+       s.storeHealthyConsistentHash(hash)
+}
+
+func (s *EndpointSnapshot) reuseHealthyConsistentHashFrom(previous 
*EndpointSnapshot) {
+       if !canReuseHealthyConsistentHash(previous, s) {
+               return
+       }
+       s.seedHealthyConsistentHash(previous.cachedHealthyConsistentHash())
+}
+
+func canReuseHealthyConsistentHash(previous, next *EndpointSnapshot) bool {
+       if previous == nil || next == nil {
+               return false
+       }
+       if previous.lbPolicy != next.lbPolicy {
+               return false
+       }
+       if _, ok := model.ConsistentHashInitMap[next.lbPolicy]; !ok {
+               return false
+       }
+       if !sameConsistentHashConfig(previous.consistentHashConfig, 
next.consistentHashConfig) {
+               return false
+       }
+       return sameHealthyEndpointsForConsistentHash(previous, next)
+}
+
+func sameConsistentHashConfig(a, b model.ConsistentHash) bool {
+       return a.ReplicaNum == b.ReplicaNum &&
+               a.MaxVnodeNum == b.MaxVnodeNum &&
+               a.MaglevTableSize == b.MaglevTableSize
+}
+
+func sameHealthyEndpointsForConsistentHash(previous, next *EndpointSnapshot) 
bool {
+       if previous == nil || next == nil {
+               return false
+       }
+       if len(previous.healthy) != len(next.healthy) {
+               return false
+       }
+       for i := range previous.healthy {
+               if !sameEndpointHostForConsistentHash(previous.healthy[i], 
next.healthy[i]) {
+                       return false
+               }
+       }
+       return true
+}
+
+func sameEndpointHostForConsistentHash(a, b *model.Endpoint) bool {
+       if a == nil || b == nil {
+               return a == b
+       }
+       return a.Address.Address == b.Address.Address && a.Address.Port == 
b.Address.Port
 }
diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go
index d8f7561d6..196c9da4a 100644
--- a/pkg/cluster/cluster_test.go
+++ b/pkg/cluster/cluster_test.go
@@ -189,6 +189,8 @@ func 
TestClusterEndpointSnapshotExposesReadOnlyConsistentHash(t *testing.T) {
 func TestClusterEndpointSnapshotBuildsConsistentHashLazily(t *testing.T) {
        var builds int32
        lbPolicy := model.LbPolicyType("test-lazy-hash")
+       // Mutates the global ConsistentHashInitMap; keep usage serial
+       // and do not add t.Parallel.
        previousInit, hadPreviousInit := model.ConsistentHashInitMap[lbPolicy]
        model.ConsistentHashInitMap[lbPolicy] = func(_ model.ConsistentHash, 
endpoints []*model.Endpoint) model.LbConsistentHash {
                atomic.AddInt32(&builds, 1)
@@ -247,6 +249,152 @@ func 
TestClusterEndpointSnapshotBuildsConsistentHashLazily(t *testing.T) {
                "build count is monotonic, but per-flap rebuild count is not 
locked here")
 }
 
+func TestClusterEndpointSnapshotReusesConsistentHashForUnchangedHealthySet(t 
*testing.T) {
+       var builds int32
+       lbPolicy := registerCountingConsistentHash(t, 
"test-reuse-hash-unchanged", &builds)
+
+       first := testEndpoint("ep-1", "127.0.0.1", 18080)
+       second := testEndpoint("ep-2", "127.0.0.1", 18081)
+       config := testCluster("snapshot-reuse-hash-unchanged", first, second)
+       config.LbStr = lbPolicy
+       config.ConsistentHash = model.ConsistentHash{ReplicaNum: 10, 
MaxVnodeNum: 1023, MaglevTableSize: 521}
+
+       previous := NewCluster(config).EndpointSnapshot()
+       previousHash := previous.HealthyConsistentHash()
+       assert.NotNil(t, previousHash)
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds))
+
+       next := newEndpointSnapshot(config, previous, false)
+       assert.Equal(t, previousHash, next.HealthyConsistentHash(),
+               "equivalent snapshot should reuse the previous hash view")
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds),
+               "reused snapshot must not rebuild on first access")
+}
+
+func 
TestClusterEndpointSnapshotDoesNotReuseConsistentHashWhenHealthChangesHealthySet(t
 *testing.T) {
+       var builds int32
+       lbPolicy := registerCountingConsistentHash(t, 
"test-reuse-hash-health-change", &builds)
+
+       first := testEndpoint("ep-1", "127.0.0.1", 18080)
+       second := testEndpoint("ep-2", "127.0.0.1", 18081)
+       config := testCluster("snapshot-reuse-hash-health-change", first, 
second)
+       config.LbStr = lbPolicy
+
+       previous := NewCluster(config).EndpointSnapshot()
+       assert.NotNil(t, previous.HealthyConsistentHash())
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds))
+
+       next, ok := 
previous.withEndpointHealthForIDs(map[string]struct{}{second.ID: {}}, false)
+       assert.True(t, ok)
+       assert.NotNil(t, next.HealthyConsistentHash())
+       assert.Equal(t, int32(2), atomic.LoadInt32(&builds),
+               "health change that changes the healthy set must force a fresh 
consistent hash")
+}
+
+func 
TestClusterEndpointSnapshotDoesNotReuseConsistentHashWhenEndpointAddressChanges(t
 *testing.T) {
+       var builds int32
+       lbPolicy := registerCountingConsistentHash(t, 
"test-reuse-hash-address-change", &builds)
+
+       endpoint := testEndpoint("ep-1", "127.0.0.1", 18080)
+       config := testCluster("snapshot-reuse-hash-address-change", endpoint)
+       config.LbStr = lbPolicy
+
+       previous := NewCluster(config).EndpointSnapshot()
+       assert.NotNil(t, previous.HealthyConsistentHash())
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds))
+
+       config.Endpoints[0] = testEndpoint("ep-1", "127.0.0.2", 18080)
+       next := newEndpointSnapshot(config, previous, false)
+       assert.NotNil(t, next.HealthyConsistentHash())
+       assert.Equal(t, int32(2), atomic.LoadInt32(&builds),
+               "address change must force a fresh consistent hash")
+}
+
+func 
TestClusterEndpointSnapshotDoesNotReuseConsistentHashWhenEndpointCountChanges(t 
*testing.T) {
+       tests := []struct {
+               name      string
+               endpoints func(first, second, third *model.Endpoint) 
[]*model.Endpoint
+       }{
+               {
+                       name: "add",
+                       endpoints: func(first, second, third *model.Endpoint) 
[]*model.Endpoint {
+                               return []*model.Endpoint{first, second, third}
+                       },
+               },
+               {
+                       name: "delete",
+                       endpoints: func(first, second, third *model.Endpoint) 
[]*model.Endpoint {
+                               return []*model.Endpoint{first}
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       var builds int32
+                       lbPolicy := registerCountingConsistentHash(t, 
"test-reuse-hash-count-change"+tt.name, &builds)
+
+                       first := testEndpoint("ep-1", "127.0.0.1", 18080)
+                       second := testEndpoint("ep-2", "127.0.0.1", 18081)
+                       third := testEndpoint("ep-3", "127.0.0.1", 18082)
+                       config := 
testCluster("snapshot-reuse-hash-count-change-"+tt.name, first, second)
+                       config.LbStr = lbPolicy
+
+                       previous := NewCluster(config).EndpointSnapshot()
+                       assert.NotNil(t, previous.HealthyConsistentHash())
+                       assert.Equal(t, int32(1), atomic.LoadInt32(&builds))
+
+                       config.Endpoints = tt.endpoints(first, second, third)
+                       next := newEndpointSnapshot(config, previous, false)
+                       assert.NotNil(t, next.HealthyConsistentHash())
+                       assert.Equal(t, int32(2), atomic.LoadInt32(&builds),
+                               "count change must force a fresh consistent 
hash")
+               })
+       }
+}
+
+func 
TestClusterEndpointSnapshotDoesNotReuseConsistentHashWhenHashConfigChanges(t 
*testing.T) {
+       var builds int32
+       lbPolicy := registerCountingConsistentHash(t, 
"test-reuse-hash-config-change", &builds)
+
+       endpoint := testEndpoint("ep-1", "127.0.0.1", 18080)
+       config := testCluster("snapshot-reuse-hash-config-change", endpoint)
+       config.LbStr = lbPolicy
+       config.ConsistentHash = model.ConsistentHash{ReplicaNum: 10, 
MaxVnodeNum: 1023, MaglevTableSize: 521}
+
+       previous := NewCluster(config).EndpointSnapshot()
+       assert.NotNil(t, previous.HealthyConsistentHash())
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds))
+
+       config.ConsistentHash.ReplicaNum = 20
+       next := newEndpointSnapshot(config, previous, false)
+       assert.NotNil(t, next.HealthyConsistentHash())
+       assert.Equal(t, int32(2), atomic.LoadInt32(&builds),
+               "hash config change must force a fresh consistent hash")
+}
+
+func TestClusterEndpointSnapshotReusesConsistentHashWhenMetadataChanges(t 
*testing.T) {
+       var builds int32
+       lbPolicy := registerCountingConsistentHash(t, 
"test-reuse-hash-metadata-change", &builds)
+
+       endpoint := testEndpoint("ep-1", "127.0.0.1", 18080)
+       endpoint.Metadata = map[string]string{"weight": "10"}
+       config := testCluster("snapshot-reuse-hash-metadata-change", endpoint)
+       config.LbStr = lbPolicy
+
+       previous := NewCluster(config).EndpointSnapshot()
+       assert.NotNil(t, previous.HealthyConsistentHash())
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds))
+
+       movedWeight := testEndpoint("ep-1", "127.0.0.1", 18080)
+       movedWeight.Metadata = map[string]string{"weight": "20"}
+       config.Endpoints[0] = movedWeight
+       next := newEndpointSnapshot(config, previous, false)
+       assert.NotNil(t, next.HealthyConsistentHash())
+       assert.Equal(t, int32(1), atomic.LoadInt32(&builds),
+               "metadata-only change with unchanged hosts must reuse the 
previous consistent hash instead of rebuilding")
+}
+
 func TestClusterEndpointSnapshotClonesConfigEndpointObjects(t *testing.T) {
        endpoint := testSnapshotEndpointWithLLMMeta()
        runtimeCluster := NewCluster(testCluster("snapshot-clone", endpoint))
@@ -559,6 +707,30 @@ func testSnapshotEndpointWithLLMMeta() *model.Endpoint {
        return endpoint
 }
 
+// Mutates the global ConsistentHashInitMap; keep usage serial
+// and do not add t.Parallel.
+func registerCountingConsistentHash(t *testing.T, name string, builds *int32) 
model.LbPolicyType {
+       t.Helper()
+
+       lbPolicy := model.LbPolicyType(name)
+       previousInit, hadPreviousInit := model.ConsistentHashInitMap[lbPolicy]
+       model.ConsistentHashInitMap[lbPolicy] = func(_ model.ConsistentHash, 
endpoints []*model.Endpoint) model.LbConsistentHash {
+               atomic.AddInt32(builds, 1)
+               if len(endpoints) == 0 {
+                       return countingConsistentHash{}
+               }
+               return countingConsistentHash{host: endpoints[0].GetHost()}
+       }
+       t.Cleanup(func() {
+               if hadPreviousInit {
+                       model.ConsistentHashInitMap[lbPolicy] = previousInit
+                       return
+               }
+               delete(model.ConsistentHashInitMap, lbPolicy)
+       })
+       return lbPolicy
+}
+
 type countingConsistentHash struct {
        host string
 }
diff --git a/pkg/server/cluster_manager_bench_test.go 
b/pkg/server/cluster_manager_bench_test.go
index 63faf3db0..054b0a3ee 100644
--- a/pkg/server/cluster_manager_bench_test.go
+++ b/pkg/server/cluster_manager_bench_test.go
@@ -34,9 +34,10 @@ import (
 
 var (
        // Keep benchmark results live so the compiler cannot optimize the hot 
path away.
-       benchmarkEndpointSink  *model.Endpoint
-       benchmarkEndpointsSink []*model.Endpoint
-       benchmarkClusterSink   *model.ClusterConfig
+       benchmarkEndpointSink       *model.Endpoint
+       benchmarkEndpointsSink      []*model.Endpoint
+       benchmarkClusterSink        *model.ClusterConfig
+       benchmarkConsistentHashSink model.LbConsistentHashView
 )
 
 type benchmarkHashPolicy string
@@ -241,6 +242,50 @@ func BenchmarkClusterConsistentHashResolve(b *testing.B) {
        }
 }
 
+func BenchmarkClusterConsistentHashSnapshotRefreshUnchangedHealthySet(b 
*testing.B) {
+       for _, lbType := range 
[]model.LbPolicyType{model.LoadBalancerRingHashing, 
model.LoadBalancerMaglevHashing} {
+               for _, endpointCount := range []int{1, 32, 256, 1024} {
+                       name := fmt.Sprintf("%s/endpoints=%d", lbType, 
endpointCount)
+                       b.Run(name, func(b *testing.B) {
+                               b.Run("reuse-cached-previous", func(b 
*testing.B) {
+                                       
benchmarkConsistentHashSnapshotRefresh(b, lbType, endpointCount, true)
+                               })
+                               b.Run("rebuild-uncached-previous", func(b 
*testing.B) {
+                                       
benchmarkConsistentHashSnapshotRefresh(b, lbType, endpointCount, false)
+                               })
+                       })
+               }
+       }
+}
+
+func benchmarkConsistentHashSnapshotRefresh(
+       b *testing.B,
+       lbType model.LbPolicyType,
+       endpointCount int,
+       previousHashBuilt bool,
+) {
+       config := benchmarkClusterConfig("consistent-hash-refresh", lbType, 
endpointCount, 0)
+       previous := cluster.NewCluster(config).EndpointSnapshot()
+       if previousHashBuilt {
+               benchmarkConsistentHashSink = previous.HealthyConsistentHash()
+               if benchmarkConsistentHashSink == nil {
+                       b.Fatal("expected previous consistent hash")
+               }
+       }
+
+       b.ReportAllocs()
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               runtimeCluster := 
cluster.NewClusterWithEndpointSnapshot(config, previous)
+               next := runtimeCluster.EndpointSnapshot()
+               benchmarkConsistentHashSink = next.HealthyConsistentHash()
+       }
+       b.StopTimer()
+       if benchmarkConsistentHashSink == nil {
+               b.Fatal("expected consistent hash")
+       }
+}
+
 func benchmarkClusterManager(clusterCount int, endpointCount int, lbType 
model.LbPolicyType) (*ClusterManager, []string) {
        clusters := make([]*model.ClusterConfig, 0, clusterCount)
        names := make([]string, 0, clusterCount)

Reply via email to