mirror of
https://github.com/redis/go-redis.git
synced 2025-07-18 00:20:57 +03:00
fix(txpipeline): keyless commands should take the slot of the keyed (#3411)
* fix(txpipeline): keyless commands should take the slot of the keyed commands * fix(txpipeline): extract only keyed cmds from all cmds * chore(test): Add tests for keyless cmds and txpipeline * fix(cmdSlot): Add preferred random slot * fix(cmdSlot): Add shortlist of keyless cmds * chore(test): Fix ring test * fix(keylessCommands): Add list of keyless commands Add list of keyless Commands based on the Commands output for redis 8 * chore(txPipeline): refactor slottedCommands impl * fix(osscluster): typo
This commit is contained in:
61
command.go
61
command.go
@ -17,6 +17,55 @@ import (
|
|||||||
"github.com/redis/go-redis/v9/internal/util"
|
"github.com/redis/go-redis/v9/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// keylessCommands contains Redis commands that have empty key specifications (9th slot empty)
|
||||||
|
// Only includes core Redis commands, excludes FT.*, ts.*, timeseries.*, search.* and subcommands
|
||||||
|
var keylessCommands = map[string]struct{}{
|
||||||
|
"acl": {},
|
||||||
|
"asking": {},
|
||||||
|
"auth": {},
|
||||||
|
"bgrewriteaof": {},
|
||||||
|
"bgsave": {},
|
||||||
|
"client": {},
|
||||||
|
"cluster": {},
|
||||||
|
"config": {},
|
||||||
|
"debug": {},
|
||||||
|
"discard": {},
|
||||||
|
"echo": {},
|
||||||
|
"exec": {},
|
||||||
|
"failover": {},
|
||||||
|
"function": {},
|
||||||
|
"hello": {},
|
||||||
|
"latency": {},
|
||||||
|
"lolwut": {},
|
||||||
|
"module": {},
|
||||||
|
"monitor": {},
|
||||||
|
"multi": {},
|
||||||
|
"pfselftest": {},
|
||||||
|
"ping": {},
|
||||||
|
"psubscribe": {},
|
||||||
|
"psync": {},
|
||||||
|
"publish": {},
|
||||||
|
"pubsub": {},
|
||||||
|
"punsubscribe": {},
|
||||||
|
"quit": {},
|
||||||
|
"readonly": {},
|
||||||
|
"readwrite": {},
|
||||||
|
"replconf": {},
|
||||||
|
"replicaof": {},
|
||||||
|
"role": {},
|
||||||
|
"save": {},
|
||||||
|
"script": {},
|
||||||
|
"select": {},
|
||||||
|
"shutdown": {},
|
||||||
|
"slaveof": {},
|
||||||
|
"slowlog": {},
|
||||||
|
"subscribe": {},
|
||||||
|
"swapdb": {},
|
||||||
|
"sync": {},
|
||||||
|
"unsubscribe": {},
|
||||||
|
"unwatch": {},
|
||||||
|
}
|
||||||
|
|
||||||
type Cmder interface {
|
type Cmder interface {
|
||||||
// command name.
|
// command name.
|
||||||
// e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster".
|
// e.g. "set k v ex 10" -> "set", "cluster info" -> "cluster".
|
||||||
@ -75,12 +124,22 @@ func writeCmd(wr *proto.Writer, cmd Cmder) error {
|
|||||||
return wr.WriteArgs(cmd.Args())
|
return wr.WriteArgs(cmd.Args())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdFirstKeyPos returns the position of the first key in the command's arguments.
|
||||||
|
// If the command does not have a key, it returns 0.
|
||||||
|
// TODO: Use the data in CommandInfo to determine the first key position.
|
||||||
func cmdFirstKeyPos(cmd Cmder) int {
|
func cmdFirstKeyPos(cmd Cmder) int {
|
||||||
if pos := cmd.firstKeyPos(); pos != 0 {
|
if pos := cmd.firstKeyPos(); pos != 0 {
|
||||||
return int(pos)
|
return int(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cmd.Name() {
|
name := cmd.Name()
|
||||||
|
|
||||||
|
// first check if the command is keyless
|
||||||
|
if _, ok := keylessCommands[name]; ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
case "eval", "evalsha", "eval_ro", "evalsha_ro":
|
case "eval", "evalsha", "eval_ro", "evalsha_ro":
|
||||||
if cmd.stringArg(2) != "0" {
|
if cmd.stringArg(2) != "0" {
|
||||||
return 3
|
return 3
|
||||||
|
@ -364,15 +364,22 @@ var _ = Describe("ClusterClient", func() {
|
|||||||
It("select slot from args for GETKEYSINSLOT command", func() {
|
It("select slot from args for GETKEYSINSLOT command", func() {
|
||||||
cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200)
|
cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", 100, 200)
|
||||||
|
|
||||||
slot := client.cmdSlot(cmd)
|
slot := client.cmdSlot(cmd, -1)
|
||||||
Expect(slot).To(Equal(100))
|
Expect(slot).To(Equal(100))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("select slot from args for COUNTKEYSINSLOT command", func() {
|
It("select slot from args for COUNTKEYSINSLOT command", func() {
|
||||||
cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100)
|
cmd := NewStringSliceCmd(ctx, "cluster", "countkeysinslot", 100)
|
||||||
|
|
||||||
slot := client.cmdSlot(cmd)
|
slot := client.cmdSlot(cmd, -1)
|
||||||
Expect(slot).To(Equal(100))
|
Expect(slot).To(Equal(100))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("follows preferred random slot", func() {
|
||||||
|
cmd := NewStatusCmd(ctx, "ping")
|
||||||
|
|
||||||
|
slot := client.cmdSlot(cmd, 101)
|
||||||
|
Expect(slot).To(Equal(101))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -998,7 +998,7 @@ func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(cmd, -1)
|
||||||
var node *clusterNode
|
var node *clusterNode
|
||||||
var moved bool
|
var moved bool
|
||||||
var ask bool
|
var ask bool
|
||||||
@ -1344,9 +1344,13 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preferredRandomSlot := -1
|
||||||
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
|
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(cmd, preferredRandomSlot)
|
||||||
|
if preferredRandomSlot == -1 {
|
||||||
|
preferredRandomSlot = slot
|
||||||
|
}
|
||||||
node, err := c.slotReadOnlyNode(state, slot)
|
node, err := c.slotReadOnlyNode(state, slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1357,7 +1361,10 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(cmd, preferredRandomSlot)
|
||||||
|
if preferredRandomSlot == -1 {
|
||||||
|
preferredRandomSlot = slot
|
||||||
|
}
|
||||||
node, err := state.slotMasterNode(slot)
|
node, err := state.slotMasterNode(slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1519,18 +1526,26 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdsMap := c.mapCmdsBySlot(cmds)
|
keyedCmdsBySlot := c.slottedKeyedCommands(cmds)
|
||||||
|
slot := -1
|
||||||
|
switch len(keyedCmdsBySlot) {
|
||||||
|
case 0:
|
||||||
|
slot = hashtag.RandomSlot()
|
||||||
|
case 1:
|
||||||
|
for sl := range keyedCmdsBySlot {
|
||||||
|
slot = sl
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
// TxPipeline does not support cross slot transaction.
|
// TxPipeline does not support cross slot transaction.
|
||||||
if len(cmdsMap) > 1 {
|
|
||||||
setCmdsErr(cmds, ErrCrossSlot)
|
setCmdsErr(cmds, ErrCrossSlot)
|
||||||
return ErrCrossSlot
|
return ErrCrossSlot
|
||||||
}
|
}
|
||||||
|
|
||||||
for slot, cmds := range cmdsMap {
|
|
||||||
node, err := state.slotMasterNode(slot)
|
node, err := state.slotMasterNode(slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setCmdsErr(cmds, err)
|
setCmdsErr(cmds, err)
|
||||||
continue
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdsMap := map[*clusterNode][]Cmder{node: cmds}
|
cmdsMap := map[*clusterNode][]Cmder{node: cmds}
|
||||||
@ -1559,18 +1574,30 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
|
|||||||
}
|
}
|
||||||
cmdsMap = failedCmds.m
|
cmdsMap = failedCmds.m
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return cmdsFirstErr(cmds)
|
return cmdsFirstErr(cmds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder {
|
// slottedKeyedCommands returns a map of slot to commands taking into account
|
||||||
cmdsMap := make(map[int][]Cmder)
|
// only commands that have keys.
|
||||||
|
func (c *ClusterClient) slottedKeyedCommands(cmds []Cmder) map[int][]Cmder {
|
||||||
|
cmdsSlots := map[int][]Cmder{}
|
||||||
|
|
||||||
|
preferredRandomSlot := -1
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
slot := c.cmdSlot(cmd)
|
if cmdFirstKeyPos(cmd) == 0 {
|
||||||
cmdsMap[slot] = append(cmdsMap[slot], cmd)
|
continue
|
||||||
}
|
}
|
||||||
return cmdsMap
|
|
||||||
|
slot := c.cmdSlot(cmd, preferredRandomSlot)
|
||||||
|
if preferredRandomSlot == -1 {
|
||||||
|
preferredRandomSlot = slot
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdsSlots[slot] = append(cmdsSlots[slot], cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmdsSlots
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) processTxPipelineNode(
|
func (c *ClusterClient) processTxPipelineNode(
|
||||||
@ -1885,17 +1912,20 @@ func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) cmdSlot(cmd Cmder) int {
|
func (c *ClusterClient) cmdSlot(cmd Cmder, preferredRandomSlot int) int {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") {
|
if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") {
|
||||||
return args[2].(int)
|
return args[2].(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmdSlot(cmd, cmdFirstKeyPos(cmd))
|
return cmdSlot(cmd, cmdFirstKeyPos(cmd), preferredRandomSlot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdSlot(cmd Cmder, pos int) int {
|
func cmdSlot(cmd Cmder, pos int, preferredRandomSlot int) int {
|
||||||
if pos == 0 {
|
if pos == 0 {
|
||||||
|
if preferredRandomSlot != -1 {
|
||||||
|
return preferredRandomSlot
|
||||||
|
}
|
||||||
return hashtag.RandomSlot()
|
return hashtag.RandomSlot()
|
||||||
}
|
}
|
||||||
firstKey := cmd.stringArg(pos)
|
firstKey := cmd.stringArg(pos)
|
||||||
|
@ -603,6 +603,15 @@ var _ = Describe("ClusterClient", func() {
|
|||||||
Expect(err).To(MatchError(redis.ErrCrossSlot))
|
Expect(err).To(MatchError(redis.ErrCrossSlot))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("works normally with keyless commands and no CrossSlot error", func() {
|
||||||
|
pipe.Set(ctx, "A{s}", "A_value", 0)
|
||||||
|
pipe.Ping(ctx)
|
||||||
|
pipe.Set(ctx, "B{s}", "B_value", 0)
|
||||||
|
pipe.Ping(ctx)
|
||||||
|
_, err := pipe.Exec(ctx)
|
||||||
|
Expect(err).To(Not(HaveOccurred()))
|
||||||
|
})
|
||||||
|
|
||||||
// doesn't fail when no commands are queued
|
// doesn't fail when no commands are queued
|
||||||
It("returns no error when there are no commands", func() {
|
It("returns no error when there are no commands", func() {
|
||||||
_, err := pipe.Exec(ctx)
|
_, err := pipe.Exec(ctx)
|
||||||
|
12
ring_test.go
12
ring_test.go
@ -304,7 +304,7 @@ var _ = Describe("Redis Ring", func() {
|
|||||||
ring = redis.NewRing(opt)
|
ring = redis.NewRing(opt)
|
||||||
})
|
})
|
||||||
It("supports Process hook", func() {
|
It("supports Process hook", func() {
|
||||||
err := ring.Ping(ctx).Err()
|
err := ring.Set(ctx, "key", "test", 0).Err()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
var stack []string
|
var stack []string
|
||||||
@ -312,12 +312,12 @@ var _ = Describe("Redis Ring", func() {
|
|||||||
ring.AddHook(&hook{
|
ring.AddHook(&hook{
|
||||||
processHook: func(hook redis.ProcessHook) redis.ProcessHook {
|
processHook: func(hook redis.ProcessHook) redis.ProcessHook {
|
||||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||||
Expect(cmd.String()).To(Equal("ping: "))
|
Expect(cmd.String()).To(Equal("get key: "))
|
||||||
stack = append(stack, "ring.BeforeProcess")
|
stack = append(stack, "ring.BeforeProcess")
|
||||||
|
|
||||||
err := hook(ctx, cmd)
|
err := hook(ctx, cmd)
|
||||||
|
|
||||||
Expect(cmd.String()).To(Equal("ping: PONG"))
|
Expect(cmd.String()).To(Equal("get key: test"))
|
||||||
stack = append(stack, "ring.AfterProcess")
|
stack = append(stack, "ring.AfterProcess")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@ -329,12 +329,12 @@ var _ = Describe("Redis Ring", func() {
|
|||||||
shard.AddHook(&hook{
|
shard.AddHook(&hook{
|
||||||
processHook: func(hook redis.ProcessHook) redis.ProcessHook {
|
processHook: func(hook redis.ProcessHook) redis.ProcessHook {
|
||||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||||
Expect(cmd.String()).To(Equal("ping: "))
|
Expect(cmd.String()).To(Equal("get key: "))
|
||||||
stack = append(stack, "shard.BeforeProcess")
|
stack = append(stack, "shard.BeforeProcess")
|
||||||
|
|
||||||
err := hook(ctx, cmd)
|
err := hook(ctx, cmd)
|
||||||
|
|
||||||
Expect(cmd.String()).To(Equal("ping: PONG"))
|
Expect(cmd.String()).To(Equal("get key: test"))
|
||||||
stack = append(stack, "shard.AfterProcess")
|
stack = append(stack, "shard.AfterProcess")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@ -344,7 +344,7 @@ var _ = Describe("Redis Ring", func() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
err = ring.Ping(ctx).Err()
|
err = ring.Get(ctx, "key").Err()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(stack).To(Equal([]string{
|
Expect(stack).To(Equal([]string{
|
||||||
"ring.BeforeProcess",
|
"ring.BeforeProcess",
|
||||||
|
Reference in New Issue
Block a user