fix: fixes ring.SetAddrs and rebalance race (#2283)

* fix: fixes ring.SetAddrs and rebalance race

The change ensures atomic update of `c.hash` and `c.shards`.

`BenchmarkRingRebalanceLocked` shows rebalance latency:
```
go test . -run=NONE -bench=BenchmarkRingRebalanceLocked -v -count=10 | benchstat /dev/stdin
name                   time/op
RingRebalanceLocked-8  8.50µs ±14%
```

(Note: it essentially reverts a46b053aa6)
This commit is contained in:
Alexander Yastrebov 2022-11-21 10:00:00 +01:00 committed by GitHub
parent bbff4dd5dc
commit d83436b321
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 24 deletions

View File

@ -95,9 +95,6 @@ func GetSlavesAddrByName(ctx context.Context, c *SentinelClient, name string) []
} }
func (c *Ring) ShardByName(name string) *ringShard { func (c *Ring) ShardByName(name string) *ringShard {
return c.sharding.ShardByName(name) shard, _ := c.sharding.GetByName(name)
} return shard
func (c *ringSharding) ShardByName(name string) *ringShard {
return c.shards.m[name]
} }

View File

@ -1,6 +1,10 @@
package redis package redis
import ( import (
"fmt"
"testing"
"time"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@ -65,3 +69,92 @@ var _ = Describe("newClusterState", func() {
}) })
}) })
}) })
type fixedHash string
func (h fixedHash) Get(string) string {
return string(h)
}
func TestRingSetAddrsAndRebalanceRace(t *testing.T) {
const (
ringShard1Name = "ringShardOne"
ringShard2Name = "ringShardTwo"
ringShard1Port = "6390"
ringShard2Port = "6391"
)
ring := NewRing(&RingOptions{
Addrs: map[string]string{
ringShard1Name: ":" + ringShard1Port,
},
// Disable heartbeat
HeartbeatFrequency: 1 * time.Hour,
NewConsistentHash: func(shards []string) ConsistentHash {
switch len(shards) {
case 1:
return fixedHash(ringShard1Name)
case 2:
return fixedHash(ringShard2Name)
default:
t.Fatalf("Unexpected number of shards: %v", shards)
return nil
}
},
})
// Continuously update addresses by adding and removing one address
updatesDone := make(chan struct{})
defer func() { close(updatesDone) }()
go func() {
for i := 0; ; i++ {
select {
case <-updatesDone:
return
default:
if i%2 == 0 {
ring.SetAddrs(map[string]string{
ringShard1Name: ":" + ringShard1Port,
})
} else {
ring.SetAddrs(map[string]string{
ringShard1Name: ":" + ringShard1Port,
ringShard2Name: ":" + ringShard2Port,
})
}
}
}
}()
timer := time.NewTimer(1 * time.Second)
for running := true; running; {
select {
case <-timer.C:
running = false
default:
shard, err := ring.sharding.GetByKey("whatever")
if err == nil && shard == nil {
t.Fatal("shard is nil")
}
}
}
}
func BenchmarkRingShardingRebalanceLocked(b *testing.B) {
opts := &RingOptions{
Addrs: make(map[string]string),
// Disable heartbeat
HeartbeatFrequency: 1 * time.Hour,
}
for i := 0; i < 100; i++ {
opts.Addrs[fmt.Sprintf("shard%d", i)] = fmt.Sprintf(":63%02d", i)
}
ring := NewRing(opts)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ring.sharding.rebalanceLocked()
}
}

32
ring.go
View File

@ -254,9 +254,9 @@ func (c *ringSharding) SetAddrs(addrs map[string]string) {
shards, cleanup := c.newRingShards(addrs, c.shards) shards, cleanup := c.newRingShards(addrs, c.shards)
c.shards = shards c.shards = shards
c.rebalanceLocked()
c.mu.Unlock() c.mu.Unlock()
c.rebalance()
cleanup() cleanup()
} }
@ -388,7 +388,9 @@ func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) {
} }
if rebalance { if rebalance {
c.rebalance() c.mu.Lock()
c.rebalanceLocked()
c.mu.Unlock()
} }
case <-ctx.Done(): case <-ctx.Done():
return return
@ -396,32 +398,26 @@ func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) {
} }
} }
// rebalance removes dead shards from the Ring. // rebalanceLocked removes dead shards from the Ring.
func (c *ringSharding) rebalance() { // Requires c.mu locked.
c.mu.RLock() func (c *ringSharding) rebalanceLocked() {
shards := c.shards if c.closed {
c.mu.RUnlock() return
}
if shards == nil { if c.shards == nil {
return return
} }
liveShards := make([]string, 0, len(shards.m)) liveShards := make([]string, 0, len(c.shards.m))
for name, shard := range shards.m { for name, shard := range c.shards.m {
if shard.IsUp() { if shard.IsUp() {
liveShards = append(liveShards, name) liveShards = append(liveShards, name)
} }
} }
hash := c.opt.NewConsistentHash(liveShards) c.hash = c.opt.NewConsistentHash(liveShards)
c.mu.Lock()
if !c.closed {
c.hash = hash
c.numShard = len(liveShards) c.numShard = len(liveShards)
}
c.mu.Unlock()
} }
func (c *ringSharding) Len() int { func (c *ringSharding) Len() int {