This is an automated email from the ASF dual-hosted git repository.
lxfeng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git
The following commit(s) were added to refs/heads/master by this push:
new 1a031ae3 fix: synchronize consistent hash refresh (#1081)
1a031ae3 is described below
commit 1a031ae35ad9d68e7226f99302860716db853c69
Author: Zhifan C <[email protected]>
AuthorDate: Thu Apr 2 11:22:19 2026 +0800
fix: synchronize consistent hash refresh (#1081)
* fix: synchronize consistent hash refresh
* test: add concurrent race coverage for consistent hash refresh
---------
Co-authored-by: Konnyaku <[email protected]>
---
.../loadbalance/consistent_hash_loadbalance.go | 95 +++++++--------
.../consistent_hash_loadbalance_test.go | 132 +++++++++++++++++++++
2 files changed, 180 insertions(+), 47 deletions(-)
diff --git a/pkg/remoting/loadbalance/consistent_hash_loadbalance.go
b/pkg/remoting/loadbalance/consistent_hash_loadbalance.go
index 54f696c3..6875e445 100644
--- a/pkg/remoting/loadbalance/consistent_hash_loadbalance.go
+++ b/pkg/remoting/loadbalance/consistent_hash_loadbalance.go
@@ -40,12 +40,6 @@ type Consistent struct {
sortedHashNodes []int64
}
-func (c *Consistent) put(key int64, session getty.Session) {
- c.Lock()
- defer c.Unlock()
- c.hashCircle[key] = session
-}
-
func (c *Consistent) hash(key string) int64 {
hashByte := md5.Sum([]byte(key))
var res int64
@@ -57,28 +51,42 @@ func (c *Consistent) hash(key string) int64 {
return res
}
-// pick get a node
-func (c *Consistent) pick(sessions *sync.Map, key string) getty.Session {
- hashKey := c.hash(key)
+func (c *Consistent) pickByHash(hashKey int64) getty.Session {
+ c.RLock()
+ defer c.RUnlock()
+
+ if len(c.sortedHashNodes) == 0 {
+ return nil
+ }
+
index := sort.Search(len(c.sortedHashNodes), func(i int) bool {
return c.sortedHashNodes[i] >= hashKey
})
-
if index == len(c.sortedHashNodes) {
- return RandomLoadBalance(sessions, key)
+ index = 0
}
- c.RLock()
- session, ok := c.hashCircle[c.sortedHashNodes[index]]
- if !ok {
- c.RUnlock()
- return RandomLoadBalance(sessions, key)
+ return c.hashCircle[c.sortedHashNodes[index]]
+}
+
+// pick get a node
+func (c *Consistent) pick(sessions *sync.Map, key string) getty.Session {
+ hashKey := c.hash(key)
+ session := c.pickByHash(hashKey)
+ if session == nil {
+ c.refreshHashCircle(sessions)
+ session = c.pickByHash(hashKey)
+ if session == nil {
+ return RandomLoadBalance(sessions, key)
+ }
}
- c.RUnlock()
if session.IsClosed() {
- go c.refreshHashCircle(sessions)
- return c.firstKey()
+ c.refreshHashCircle(sessions)
+ session = c.pickByHash(hashKey)
+ if session == nil || session.IsClosed() {
+ return RandomLoadBalance(sessions, key)
+ }
}
return session
@@ -86,32 +94,42 @@ func (c *Consistent) pick(sessions *sync.Map, key string)
getty.Session {
// refreshHashCircle refresh hashCircle
func (c *Consistent) refreshHashCircle(sessions *sync.Map) {
- var sortedHashNodes []int64
- hashCircle := make(map[int64]getty.Session)
- var session getty.Session
- c.RLock()
- defer c.RUnlock()
+ var (
+ sortedHashNodes []int64
+ hashCircle = make(map[int64]getty.Session)
+ closedSessions []interface{}
+ )
+
sessions.Range(func(key, value interface{}) bool {
- session = key.(getty.Session)
+ session := key.(getty.Session)
+ if session.IsClosed() {
+ closedSessions = append(closedSessions, key)
+ return true
+ }
+
for i := 0; i < defaultVirtualNodeNumber; i++ {
if !session.IsClosed() {
position := c.hash(fmt.Sprintf("%s%d",
session.RemoteAddr(), i))
hashCircle[position] = session
sortedHashNodes = append(sortedHashNodes,
position)
- } else {
- sessions.Delete(key)
}
}
return true
})
+ for _, session := range closedSessions {
+ sessions.Delete(session)
+ }
+
// virtual node sort
sort.Slice(sortedHashNodes, func(i, j int) bool {
return sortedHashNodes[i] < sortedHashNodes[j]
})
+ c.Lock()
c.sortedHashNodes = sortedHashNodes
c.hashCircle = hashCircle
+ c.Unlock()
}
func (c *Consistent) firstKey() getty.Session {
@@ -128,27 +146,10 @@ func (c *Consistent) firstKey() getty.Session {
func newConsistenceInstance(sessions *sync.Map) *Consistent {
once.Do(func() {
consistentInstance = &Consistent{
- hashCircle: make(map[int64]getty.Session),
+ virtualNodeCount: defaultVirtualNodeNumber,
+ hashCircle: make(map[int64]getty.Session),
}
- // construct hash circle
- sessions.Range(func(key, value interface{}) bool {
- session := key.(getty.Session)
- for i := 0; i < defaultVirtualNodeNumber; i++ {
- if !session.IsClosed() {
- position :=
consistentInstance.hash(fmt.Sprintf("%s%d", session.RemoteAddr(), i))
- consistentInstance.put(position,
session)
- consistentInstance.sortedHashNodes =
append(consistentInstance.sortedHashNodes, position)
- } else {
- sessions.Delete(key)
- }
- }
- return true
- })
-
- // virtual node sort
- sort.Slice(consistentInstance.sortedHashNodes, func(i, j int)
bool {
- return consistentInstance.sortedHashNodes[i] <
consistentInstance.sortedHashNodes[j]
- })
+ consistentInstance.refreshHashCircle(sessions)
})
return consistentInstance
diff --git a/pkg/remoting/loadbalance/consistent_hash_loadbalance_test.go
b/pkg/remoting/loadbalance/consistent_hash_loadbalance_test.go
index 250a2638..24e60edf 100644
--- a/pkg/remoting/loadbalance/consistent_hash_loadbalance_test.go
+++ b/pkg/remoting/loadbalance/consistent_hash_loadbalance_test.go
@@ -20,15 +20,25 @@ package loadbalance
import (
"fmt"
"sync"
+ "sync/atomic"
"testing"
+ getty "github.com/apache/dubbo-getty"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"seata.apache.org/seata-go/v2/pkg/remoting/mock"
)
+func resetConsistentHashForTest() {
+ consistentInstance = nil
+ once = sync.Once{}
+}
+
func TestConsistentHashLoadBalance(t *testing.T) {
+ resetConsistentHashForTest()
+ defer resetConsistentHashForTest()
+
ctrl := gomock.NewController(t)
sessions := &sync.Map{}
@@ -50,3 +60,125 @@ func TestConsistentHashLoadBalance(t *testing.T) {
return true
})
}
+
+func TestConsistentPick_RefreshesClosedSession(t *testing.T) {
+ resetConsistentHashForTest()
+ defer resetConsistentHashForTest()
+
+ ctrl := gomock.NewController(t)
+ sessions := &sync.Map{}
+
+ closedSession := mock.NewMockTestSession(ctrl)
+ closedSession.EXPECT().IsClosed().AnyTimes().Return(true)
+
+ openSession := mock.NewMockTestSession(ctrl)
+ openSession.EXPECT().IsClosed().AnyTimes().Return(false)
+ openSession.EXPECT().RemoteAddr().AnyTimes().Return("127.0.0.1:8001")
+
+ sessions.Store(closedSession, "closed")
+ sessions.Store(openSession, "open")
+
+ c := &Consistent{
+ virtualNodeCount: defaultVirtualNodeNumber,
+ hashCircle: map[int64]getty.Session{
+ 1: closedSession,
+ },
+ sortedHashNodes: []int64{1},
+ }
+
+ result := c.pick(sessions, "test_xid")
+ assert.NotNil(t, result)
+ assert.Equal(t, openSession, result)
+
+ _, stillExists := sessions.Load(closedSession)
+ assert.False(t, stillExists)
+}
+
+func TestConsistentPick_ConcurrentPickAndRefresh(t *testing.T) {
+ resetConsistentHashForTest()
+ defer resetConsistentHashForTest()
+
+ ctrl := gomock.NewController(t)
+ sessions := &sync.Map{}
+
+ stableSession := mock.NewMockTestSession(ctrl)
+ stableSession.EXPECT().IsClosed().AnyTimes().Return(false)
+ stableSession.EXPECT().RemoteAddr().AnyTimes().Return("127.0.0.1:9000")
+ sessions.Store(stableSession, "stable")
+
+ type flappingSession struct {
+ session getty.Session
+ addr string
+ closed atomic.Bool
+ }
+
+ flapping := make([]*flappingSession, 0, 2)
+ for i := 0; i < 2; i++ {
+ addr := fmt.Sprintf("127.0.0.1:900%d", i+1)
+ state := &flappingSession{addr: addr}
+
+ session := mock.NewMockTestSession(ctrl)
+ session.EXPECT().IsClosed().AnyTimes().DoAndReturn(func() bool {
+ return state.closed.Load()
+ })
+ session.EXPECT().RemoteAddr().AnyTimes().Return(addr)
+
+ state.session = session
+ flapping = append(flapping, state)
+ sessions.Store(session, addr)
+ }
+
+ c := &Consistent{
+ virtualNodeCount: defaultVirtualNodeNumber,
+ hashCircle: make(map[int64]getty.Session),
+ }
+ c.refreshHashCircle(sessions)
+
+ const (
+ pickers = 8
+ iterations = 200
+ )
+
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+
+ for pickerID := 0; pickerID < pickers; pickerID++ {
+ pickerID := pickerID
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ for i := 0; i < iterations; i++ {
+ session := c.pick(sessions,
fmt.Sprintf("xid-%d-%d", pickerID, i))
+ if session == nil {
+ continue
+ }
+ _ = session.IsClosed()
+ }
+ }()
+ }
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ for i := 0; i < iterations; i++ {
+ state := flapping[i%len(flapping)]
+ if i%2 == 0 {
+ state.closed.Store(true)
+ } else {
+ state.closed.Store(false)
+ sessions.Store(state.session, state.addr)
+ }
+
+ c.refreshHashCircle(sessions)
+ }
+ }()
+
+ close(start)
+ wg.Wait()
+
+ result := c.pick(sessions, "final-xid")
+ assert.NotNil(t, result)
+ assert.False(t, result.IsClosed())
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]