Skip to content

Commit accd6ed

Browse files
committed
Set correct cluster slot for scan commands, similarly to Java's Jedis client
- At present, the `scan` command is dispatched to a random slot. - As far as I can tell, the scanX family of commands are not cluster aware (e.g. don't redirect the client to the correct slot). - You can see [here](https://github.com/redis/jedis/blob/869dc0bb6625b85c8bf15bf1361bde485a304338/src/main/java/redis/clients/jedis/ShardedCommandObjects.java#L101), the Jedis client calling `processKey` on the match argument, and this is what this PR also does. We've had this patch running in production, and it seems to work well for us. For further thought: - Continuing looking at other Redis clients (e.g. Jedis), they outright [reject as invalid](https://github.com/redis/jedis/blob/869dc0bb6625b85c8bf15bf1361bde485a304338/src/main/java/redis/clients/jedis/ShardedCommandObjects.java#L98) any scan command that does not include a hash-tag. Presumably this has the advantage of users not being surprised when their scan produces no results when a random server is picked. - Perhaps it would be sensible for go-redis to do the same also?
1 parent 4e22885 commit accd6ed

File tree

6 files changed

+66
-1
lines changed

6 files changed

+66
-1
lines changed

generic_commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package redis
33
import (
44
"context"
55
"time"
6+
7+
"github.com/redis/go-redis/v9/internal/hashtag"
68
)
79

810
type GenericCmdable interface {
@@ -363,6 +365,9 @@ func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count in
363365
args = append(args, "count", count)
364366
}
365367
cmd := NewScanCmd(ctx, c, args...)
368+
if hashtag.Present(match) {
369+
cmd.SetFirstKeyPos(3)
370+
}
366371
_ = c(ctx, cmd)
367372
return cmd
368373
}
@@ -379,6 +384,9 @@ func (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, coun
379384
args = append(args, "type", keyType)
380385
}
381386
cmd := NewScanCmd(ctx, c, args...)
387+
if hashtag.Present(match) {
388+
cmd.SetFirstKeyPos(3)
389+
}
382390
_ = c(ctx, cmd)
383391
return cmd
384392
}

hash_commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package redis
33
import (
44
"context"
55
"time"
6+
7+
"github.com/redis/go-redis/v9/internal/hashtag"
68
)
79

810
type HashCmdable interface {
@@ -192,6 +194,9 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str
192194
args = append(args, "count", count)
193195
}
194196
cmd := NewScanCmd(ctx, c, args...)
197+
if hashtag.Present(match) {
198+
cmd.SetFirstKeyPos(4)
199+
}
195200
_ = c(ctx, cmd)
196201
return cmd
197202
}
@@ -211,6 +216,9 @@ func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, m
211216
}
212217
args = append(args, "novalues")
213218
cmd := NewScanCmd(ctx, c, args...)
219+
if hashtag.Present(match) {
220+
cmd.SetFirstKeyPos(5)
221+
}
214222
_ = c(ctx, cmd)
215223
return cmd
216224
}

internal/hashtag/hashtag.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ func Key(key string) string {
5656
return key
5757
}
5858

59+
func Present(key string) bool {
60+
if key == "" {
61+
return false
62+
}
63+
if s := strings.IndexByte(key, '{'); s > -1 {
64+
if e := strings.IndexByte(key[s+1:], '}'); e > 0 {
65+
return true
66+
}
67+
}
68+
return false
69+
}
70+
5971
func RandomSlot() int {
6072
return rand.Intn(slotNumber)
6173
}

internal/hashtag/hashtag_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,28 @@ var _ = Describe("HashSlot", func() {
6969
}
7070
})
7171
})
72+
73+
var _ = Describe("Present", func() {
74+
It("should calculate hash slots", func() {
75+
tests := []struct {
76+
key string
77+
present bool
78+
}{
79+
{"123456789", false},
80+
{"{}foo", false},
81+
{"foo{}", false},
82+
{"foo{}{bar}", false},
83+
{"", false},
84+
{string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), false},
85+
{"foo{bar}", true},
86+
{"{foo}bar", true},
87+
{"{user1000}.following", true},
88+
{"foo{{bar}}zap", true},
89+
{"foo{bar}{zap}", true},
90+
}
91+
92+
for _, test := range tests {
93+
Expect(Present(test.key)).To(Equal(test.present), "for %s", test.key)
94+
}
95+
})
96+
})

set_commands.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package redis
22

3-
import "context"
3+
import (
4+
"context"
5+
6+
"github.com/redis/go-redis/v9/internal/hashtag"
7+
)
48

59
type SetCmdable interface {
610
SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd
@@ -212,6 +216,9 @@ func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match str
212216
args = append(args, "count", count)
213217
}
214218
cmd := NewScanCmd(ctx, c, args...)
219+
if hashtag.Present(match) {
220+
cmd.SetFirstKeyPos(4)
221+
}
215222
_ = c(ctx, cmd)
216223
return cmd
217224
}

sortedset_commands.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"strings"
66
"time"
7+
8+
"github.com/redis/go-redis/v9/internal/hashtag"
79
)
810

911
type SortedSetCmdable interface {
@@ -720,6 +722,9 @@ func (c cmdable) ZScan(ctx context.Context, key string, cursor uint64, match str
720722
args = append(args, "count", count)
721723
}
722724
cmd := NewScanCmd(ctx, c, args...)
725+
if hashtag.Present(match) {
726+
cmd.SetFirstKeyPos(4)
727+
}
723728
_ = c(ctx, cmd)
724729
return cmd
725730
}

0 commit comments

Comments
 (0)