Skip to content

Commit 06a4de2

Browse files
authored
Merge branch 'master' into connection_issue
2 parents 7c445ab + e63669e commit 06a4de2

File tree

6 files changed

+118
-4
lines changed

6 files changed

+118
-4
lines changed

.github/workflows/spellcheck.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
- name: Checkout
99
uses: actions/checkout@v4
1010
- name: Check Spelling
11-
uses: rojopolis/spellcheck-github-actions@0.40.0
11+
uses: rojopolis/spellcheck-github-actions@0.45.0
1212
with:
1313
config_path: .github/spellcheck-settings.yml
1414
task_name: Markdown

command.go

+2
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ func (cmd *baseCmd) stringArg(pos int) string {
167167
switch v := arg.(type) {
168168
case string:
169169
return v
170+
case []byte:
171+
return string(v)
170172
default:
171173
// TODO: consider using appendArg
172174
return fmt.Sprint(v)

osscluster.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import (
2121
"github.com/redis/go-redis/v9/internal/rand"
2222
)
2323

24+
const (
25+
minLatencyMeasurementInterval = 10 * time.Second
26+
)
27+
2428
var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes")
2529

2630
// ClusterOptions are used to configure a cluster client and should be
@@ -316,6 +320,10 @@ type clusterNode struct {
316320
latency uint32 // atomic
317321
generation uint32 // atomic
318322
failing uint32 // atomic
323+
324+
// last time the latency measurement was performed for the node, stored in nanoseconds
325+
// from epoch
326+
lastLatencyMeasurement int64 // atomic
319327
}
320328

321329
func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode {
@@ -368,6 +376,7 @@ func (n *clusterNode) updateLatency() {
368376
latency = float64(dur) / float64(successes)
369377
}
370378
atomic.StoreUint32(&n.latency, uint32(latency+0.5))
379+
n.SetLastLatencyMeasurement(time.Now())
371380
}
372381

373382
func (n *clusterNode) Latency() time.Duration {
@@ -397,6 +406,10 @@ func (n *clusterNode) Generation() uint32 {
397406
return atomic.LoadUint32(&n.generation)
398407
}
399408

409+
func (n *clusterNode) LastLatencyMeasurement() int64 {
410+
return atomic.LoadInt64(&n.lastLatencyMeasurement)
411+
}
412+
400413
func (n *clusterNode) SetGeneration(gen uint32) {
401414
for {
402415
v := atomic.LoadUint32(&n.generation)
@@ -406,6 +419,15 @@ func (n *clusterNode) SetGeneration(gen uint32) {
406419
}
407420
}
408421

422+
func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) {
423+
for {
424+
v := atomic.LoadInt64(&n.lastLatencyMeasurement)
425+
if t.UnixNano() < v || atomic.CompareAndSwapInt64(&n.lastLatencyMeasurement, v, t.UnixNano()) {
426+
break
427+
}
428+
}
429+
}
430+
409431
//------------------------------------------------------------------------------
410432

411433
type clusterNodes struct {
@@ -493,10 +515,11 @@ func (c *clusterNodes) GC(generation uint32) {
493515
c.mu.Lock()
494516

495517
c.activeAddrs = c.activeAddrs[:0]
518+
now := time.Now()
496519
for addr, node := range c.nodes {
497520
if node.Generation() >= generation {
498521
c.activeAddrs = append(c.activeAddrs, addr)
499-
if c.opt.RouteByLatency {
522+
if c.opt.RouteByLatency && node.LastLatencyMeasurement() < now.Add(-minLatencyMeasurementInterval).UnixNano() {
500523
go node.updateLatency()
501524
}
502525
continue

osscluster_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,32 @@ var _ = Describe("ClusterClient", func() {
653653
Expect(client.Close()).NotTo(HaveOccurred())
654654
})
655655

656+
It("determines hash slots correctly for generic commands", func() {
657+
opt := redisClusterOptions()
658+
opt.MaxRedirects = -1
659+
client := cluster.newClusterClient(ctx, opt)
660+
661+
err := client.Do(ctx, "GET", "A").Err()
662+
Expect(err).To(Equal(redis.Nil))
663+
664+
err = client.Do(ctx, []byte("GET"), []byte("A")).Err()
665+
Expect(err).To(Equal(redis.Nil))
666+
667+
Eventually(func() error {
668+
return client.SwapNodes(ctx, "A")
669+
}, 30*time.Second).ShouldNot(HaveOccurred())
670+
671+
err = client.Do(ctx, "GET", "A").Err()
672+
Expect(err).To(HaveOccurred())
673+
Expect(err.Error()).To(ContainSubstring("MOVED"))
674+
675+
err = client.Do(ctx, []byte("GET"), []byte("A")).Err()
676+
Expect(err).To(HaveOccurred())
677+
Expect(err.Error()).To(ContainSubstring("MOVED"))
678+
679+
Expect(client.Close()).NotTo(HaveOccurred())
680+
})
681+
656682
It("follows node redirection immediately", func() {
657683
// Configure retry backoffs far in excess of the expected duration of redirection
658684
opt := redisClusterOptions()

redis.go

-2
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,6 @@ func (hs *hooksMixin) withProcessPipelineHook(
176176
}
177177

178178
func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) {
179-
hs.hooksMu.Lock()
180-
defer hs.hooksMu.Unlock()
181179
return hs.current.dial(ctx, network, addr)
182180
}
183181

redis_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"net"
9+
"sync"
910
"testing"
1011
"time"
1112

@@ -633,3 +634,67 @@ var _ = Describe("Hook with MinIdleConns", func() {
633634
}))
634635
})
635636
})
637+
638+
var _ = Describe("Dialer connection timeouts", func() {
639+
var client *redis.Client
640+
641+
const dialSimulatedDelay = 1 * time.Second
642+
643+
BeforeEach(func() {
644+
options := redisOptions()
645+
options.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
646+
// Simulated slow dialer.
647+
// Note that the following sleep is deliberately not context-aware.
648+
time.Sleep(dialSimulatedDelay)
649+
return net.Dial("tcp", options.Addr)
650+
}
651+
options.MinIdleConns = 1
652+
client = redis.NewClient(options)
653+
})
654+
655+
AfterEach(func() {
656+
err := client.Close()
657+
Expect(err).NotTo(HaveOccurred())
658+
})
659+
660+
It("does not contend on connection dial for concurrent commands", func() {
661+
var wg sync.WaitGroup
662+
663+
const concurrency = 10
664+
665+
durations := make(chan time.Duration, concurrency)
666+
errs := make(chan error, concurrency)
667+
668+
start := time.Now()
669+
wg.Add(concurrency)
670+
671+
for i := 0; i < concurrency; i++ {
672+
go func() {
673+
defer wg.Done()
674+
675+
start := time.Now()
676+
err := client.Ping(ctx).Err()
677+
durations <- time.Since(start)
678+
errs <- err
679+
}()
680+
}
681+
682+
wg.Wait()
683+
close(durations)
684+
close(errs)
685+
686+
// All commands should eventually succeed, after acquiring a connection.
687+
for err := range errs {
688+
Expect(err).NotTo(HaveOccurred())
689+
}
690+
691+
// Each individual command should complete within the simulated dial duration bound.
692+
for duration := range durations {
693+
Expect(duration).To(BeNumerically("<", 2*dialSimulatedDelay))
694+
}
695+
696+
// Due to concurrent execution, the entire test suite should also complete within
697+
// the same dial duration bound applied for individual commands.
698+
Expect(time.Since(start)).To(BeNumerically("<", 2*dialSimulatedDelay))
699+
})
700+
})

0 commit comments

Comments
 (0)