Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ var ErrPoolExhausted = pool.ErrPoolExhausted
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
var ErrPoolTimeout = pool.ErrPoolTimeout

// ErrCrossSlot is returned when keys are used in the same Redis command and
// the keys are not in the same hash slot. This error is returned by Redis
// Cluster and will be returned by the client when TxPipeline or TxPipelined
// is used on a ClusterClient with keys in different slots.
var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot")

// HasErrorPrefix checks if the err is a Redis error and the message contains a prefix.
func HasErrorPrefix(err error, prefix string) bool {
var rErr Error
Expand Down
9 changes: 9 additions & 0 deletions osscluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,15 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
}

cmdsMap := c.mapCmdsBySlot(cmds)
// TxPipeline does not support cross slot transaction.
if len(cmdsMap) > 1 {
setCmdsErr(cmds, ErrCrossSlot)
return ErrCrossSlot
}
if len(cmdsMap) == 0 {
return nil
}

for slot, cmds := range cmdsMap {
node, err := state.slotMasterNode(slot)
if err != nil {
Expand Down
48 changes: 32 additions & 16 deletions osscluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,7 @@ var _ = Describe("ClusterClient", func() {
Describe("pipelining", func() {
var pipe *redis.Pipeline

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

It("follows redirects", func() {
if !failover {
Expand All @@ -482,13 +481,12 @@ var _ = Describe("ClusterClient", func() {
Expect(err).NotTo(HaveOccurred())
Expect(cmds).To(HaveLen(14))

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

if !failover {
for _, key := range keys {
Expand Down Expand Up @@ -517,14 +515,14 @@ var _ = Describe("ClusterClient", func() {
})

It("works with missing keys", func() {
pipe.Set(ctx, "A", "A_value", 0)
pipe.Set(ctx, "C", "C_value", 0)
pipe.Set(ctx, "A{s}", "A_value", 0)
pipe.Set(ctx, "C{s}", "C_value", 0)
_, err := pipe.Exec(ctx)
Expect(err).NotTo(HaveOccurred())

a := pipe.Get(ctx, "A")
b := pipe.Get(ctx, "B")
c := pipe.Get(ctx, "C")
a := pipe.Get(ctx, "A{s}")
b := pipe.Get(ctx, "B{s}")
c := pipe.Get(ctx, "C{s}")
cmds, err := pipe.Exec(ctx)
Expect(err).To(Equal(redis.Nil))
Expect(cmds).To(HaveLen(3))
Expand All @@ -547,7 +545,8 @@ var _ = Describe("ClusterClient", func() {

AfterEach(func() {})

assertPipeline()
keys := []string{"A", "B", "C", "D", "E", "F", "G"}
assertPipeline(keys)

It("doesn't fail node with context.Canceled error", func() {
ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -590,7 +589,24 @@ var _ = Describe("ClusterClient", func() {

AfterEach(func() {})

assertPipeline()
// TxPipeline doesn't support cross slot commands.
// Use hashtag to force all keys to the same slot.
keys := []string{"A{s}", "B{s}", "C{s}", "D{s}", "E{s}", "F{s}", "G{s}"}
assertPipeline(keys)

// make sure CrossSlot error is returned
It("returns CrossSlot error", func() {
pipe.Set(ctx, "A{s}", "A_value", 0)
pipe.Set(ctx, "B{t}", "B_value", 0)
_, err := pipe.Exec(ctx)
Expect(err).To(MatchError(redis.ErrCrossSlot))
})

// doesn't fail when no commands are queued
It("returns no error when there are no commands", func() {
_, err := pipe.Exec(ctx)
Expect(err).NotTo(HaveOccurred())
})
})
})

Expand Down
Loading