Skip to content

Commit 3e74631

Browse files
ndyakovofekshenawa
authored andcommitted
fix(txpipeline): should return error on multi/exec on multiple slots [CAE-1028] (redis#3408)
* fix(txpipeline): should return error on multi/exec on multiple slots * fix(txpipeline): test normal tx pipeline behaviour * chore(err): Extract crossslot err and add test * fix(txpipeline): short curcuit the tx if there are no commands * chore(tests): validate keys are in different slots
1 parent 2259472 commit 3e74631

File tree

3 files changed

+49
-16
lines changed

3 files changed

+49
-16
lines changed

error.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ var ErrPoolExhausted = pool.ErrPoolExhausted
2222
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
2323
var ErrPoolTimeout = pool.ErrPoolTimeout
2424

25+
// ErrCrossSlot is returned when keys are used in the same Redis command and
26+
// the keys are not in the same hash slot. This error is returned by Redis
27+
// Cluster and will be returned by the client when TxPipeline or TxPipelined
28+
// is used on a ClusterClient with keys in different slots.
29+
var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot")
30+
2531
// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.
2632
func HasErrorPrefix(err error, prefix string) bool {
2733
var rErr Error

osscluster.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,13 +1592,23 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
15921592
// Trim multi .. exec.
15931593
cmds = cmds[1 : len(cmds)-1]
15941594

1595+
if len(cmds) == 0 {
1596+
return nil
1597+
}
1598+
15951599
state, err := c.state.Get(ctx)
15961600
if err != nil {
15971601
setCmdsErr(cmds, err)
15981602
return err
15991603
}
16001604

16011605
cmdsMap := c.mapCmdsBySlot(cmds)
1606+
// TxPipeline does not support cross slot transaction.
1607+
if len(cmdsMap) > 1 {
1608+
setCmdsErr(cmds, ErrCrossSlot)
1609+
return ErrCrossSlot
1610+
}
1611+
16021612
for slot, cmds := range cmdsMap {
16031613
node, err := state.slotMasterNode(slot)
16041614
if err != nil {

osscluster_test.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,7 @@ var _ = Describe("ClusterClient", func() {
473473
Describe("pipelining", func() {
474474
var pipe *redis.Pipeline
475475

476-
assertPipeline := func() {
477-
keys := []string{"A", "B", "C", "D", "E", "F", "G"}
476+
assertPipeline := func(keys []string) {
478477

479478
It("follows redirects", func() {
480479
if !failover {
@@ -493,13 +492,12 @@ var _ = Describe("ClusterClient", func() {
493492
Expect(err).NotTo(HaveOccurred())
494493
Expect(cmds).To(HaveLen(14))
495494

496-
_ = client.ForEachShard(ctx, func(ctx context.Context, node *redis.Client) error {
497-
defer GinkgoRecover()
498-
Eventually(func() int64 {
499-
return node.DBSize(ctx).Val()
500-
}, 30*time.Second).ShouldNot(BeZero())
501-
return nil
502-
})
495+
// Check that all keys are set.
496+
for _, key := range keys {
497+
Eventually(func() string {
498+
return client.Get(ctx, key).Val()
499+
}, 30*time.Second).Should(Equal(key + "_value"))
500+
}
503501

504502
if !failover {
505503
for _, key := range keys {
@@ -528,14 +526,14 @@ var _ = Describe("ClusterClient", func() {
528526
})
529527

530528
It("works with missing keys", func() {
531-
pipe.Set(ctx, "A", "A_value", 0)
532-
pipe.Set(ctx, "C", "C_value", 0)
529+
pipe.Set(ctx, "A{s}", "A_value", 0)
530+
pipe.Set(ctx, "C{s}", "C_value", 0)
533531
_, err := pipe.Exec(ctx)
534532
Expect(err).NotTo(HaveOccurred())
535533

536-
a := pipe.Get(ctx, "A")
537-
b := pipe.Get(ctx, "B")
538-
c := pipe.Get(ctx, "C")
534+
a := pipe.Get(ctx, "A{s}")
535+
b := pipe.Get(ctx, "B{s}")
536+
c := pipe.Get(ctx, "C{s}")
539537
cmds, err := pipe.Exec(ctx)
540538
Expect(err).To(Equal(redis.Nil))
541539
Expect(cmds).To(HaveLen(3))
@@ -558,7 +556,8 @@ var _ = Describe("ClusterClient", func() {
558556

559557
AfterEach(func() {})
560558

561-
assertPipeline()
559+
keys := []string{"A", "B", "C", "D", "E", "F", "G"}
560+
assertPipeline(keys)
562561

563562
It("doesn't fail node with context.Canceled error", func() {
564563
ctx, cancel := context.WithCancel(context.Background())
@@ -601,7 +600,25 @@ var _ = Describe("ClusterClient", func() {
601600

602601
AfterEach(func() {})
603602

604-
assertPipeline()
603+
// TxPipeline doesn't support cross slot commands.
604+
// Use hashtag to force all keys to the same slot.
605+
keys := []string{"A{s}", "B{s}", "C{s}", "D{s}", "E{s}", "F{s}", "G{s}"}
606+
assertPipeline(keys)
607+
608+
// make sure CrossSlot error is returned
609+
It("returns CrossSlot error", func() {
610+
pipe.Set(ctx, "A{s}", "A_value", 0)
611+
pipe.Set(ctx, "B{t}", "B_value", 0)
612+
Expect(hashtag.Slot("A{s}")).NotTo(Equal(hashtag.Slot("B{t}")))
613+
_, err := pipe.Exec(ctx)
614+
Expect(err).To(MatchError(redis.ErrCrossSlot))
615+
})
616+
617+
// doesn't fail when no commands are queued
618+
It("returns no error when there are no commands", func() {
619+
_, err := pipe.Exec(ctx)
620+
Expect(err).NotTo(HaveOccurred())
621+
})
605622
})
606623
})
607624

0 commit comments

Comments
 (0)