Skip to content

Commit 702a6b1

Browse files
authored
Merge pull request #62 from KyberNetwork/ft/redis-scan
ft: utils to hscan only keys or values
2 parents d66e9a1 + 7c8a0e8 commit 702a6b1

File tree

4 files changed

+158
-6
lines changed

4 files changed

+158
-6
lines changed

pkg/client/redis.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/redis/go-redis/extra/redisotel/v9"
1111
"github.com/redis/go-redis/v9"
1212

13-
"github.com/KyberNetwork/service-framework/pkg/client/redis/reconnectable"
13+
reconredis "github.com/KyberNetwork/service-framework/pkg/client/redis/reconnectable"
1414
)
1515

1616
const RedisCloseDelay = time.Minute
@@ -41,24 +41,24 @@ func (*RedisCfg) OnUpdate(old, new *RedisCfg) {
4141
func NewRedisClient(ctx context.Context, opts *redis.UniversalOptions) redis.UniversalClient {
4242
if opts.MasterName == "" {
4343
return reconredis.New(func() redis.UniversalClient {
44-
return instrumentRedisOtel(ctx, redis.NewUniversalClient(opts))
44+
return InstrumentRedisOtel(ctx, redis.NewUniversalClient(opts))
4545
})
4646
}
4747
failoverOpts := opts.Failover()
4848
failoverOpts.RouteByLatency = opts.RouteByLatency
4949
failoverOpts.RouteRandomly = opts.RouteRandomly
50-
return instrumentRedisOtel(ctx, redis.NewFailoverClusterClient(failoverOpts))
50+
return InstrumentRedisOtel(ctx, redis.NewFailoverClusterClient(failoverOpts))
5151
}
5252

53-
func instrumentRedisOtel(ctx context.Context, client redis.UniversalClient) redis.UniversalClient {
53+
func InstrumentRedisOtel(ctx context.Context, client redis.UniversalClient) redis.UniversalClient {
5454
if metric.Provider() != nil {
5555
if err := redisotel.InstrumentMetrics(client); err != nil {
56-
klog.Errorf(ctx, "instrumentRedisOtel|redisotel.InstrumentMetrics failed|err=%v", err)
56+
klog.Errorf(ctx, "InstrumentRedisOtel|redisotel.InstrumentMetrics failed|err=%v", err)
5757
}
5858
}
5959
if tracer.Provider() != nil {
6060
if err := redisotel.InstrumentTracing(client); err != nil {
61-
klog.Errorf(ctx, "instrumentRedisOtel|redisotel.InstrumentTracing failed|err=%v", err)
61+
klog.Errorf(ctx, "InstrumentRedisOtel|redisotel.InstrumentTracing failed|err=%v", err)
6262
}
6363
}
6464
return client

pkg/client/redis/hscan.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package kredis
2+
3+
import (
4+
"context"
5+
6+
"github.com/redis/go-redis/v9"
7+
)
8+
9+
const LuaHscanOnlyKeys = `
10+
local results = {}
11+
local scan_result = redis.call('HSCAN', KEYS[1], unpack(ARGV))
12+
for i = 1, #scan_result[2], 2 do
13+
table.insert(results, scan_result[2][i])
14+
end
15+
return {scan_result[1], results}
16+
`
17+
18+
// HscanOnlyKeys scans only keys (no values) from a redis hash
19+
func HscanOnlyKeys(ctx context.Context, client redis.UniversalClient, key string, cursor uint64, match string,
20+
count int64) *redis.ScanCmd {
21+
args := []any{"eval", LuaHscanOnlyKeys, 1, key, cursor}
22+
if match != "" {
23+
args = append(args, "match", match)
24+
}
25+
if count > 0 {
26+
args = append(args, "count", count)
27+
}
28+
c := client.Process
29+
cmd := redis.NewScanCmd(ctx, c, args...)
30+
_ = c(ctx, cmd)
31+
return cmd
32+
}
33+
34+
const LuaHscanOnlyValues = `
35+
local results = {}
36+
local scan_result = redis.call('HSCAN', KEYS[1], unpack(ARGV))
37+
for i = 2, #scan_result[2], 2 do
38+
table.insert(results, scan_result[2][i])
39+
end
40+
return {scan_result[1], results}
41+
`
42+
43+
// HscanOnlyValues scans only values (no keys) from a redis hash
44+
func HscanOnlyValues(ctx context.Context, client redis.UniversalClient, key string, cursor uint64, match string,
45+
count int64) *redis.ScanCmd {
46+
args := []any{"eval", LuaHscanOnlyValues, 1, key, cursor}
47+
if match != "" {
48+
args = append(args, "match", match)
49+
}
50+
if count > 0 {
51+
args = append(args, "count", count)
52+
}
53+
c := client.Process
54+
cmd := redis.NewScanCmd(ctx, c, args...)
55+
_ = c(ctx, cmd)
56+
return cmd
57+
}

pkg/client/redis/hscan_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package kredis
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/alicebob/miniredis/v2"
8+
"github.com/redis/go-redis/v9"
9+
"github.com/stretchr/testify/suite"
10+
)
11+
12+
func TestTestSuite(t *testing.T) {
13+
suite.Run(t, new(TestSuite))
14+
}
15+
16+
type TestSuite struct {
17+
suite.Suite
18+
mockRedis *miniredis.Miniredis
19+
client redis.UniversalClient
20+
}
21+
22+
func (ts *TestSuite) SetupSuite() {
23+
ts.mockRedis = miniredis.RunT(ts.T())
24+
ts.client = redis.NewClient(&redis.Options{
25+
Addr: ts.mockRedis.Addr(),
26+
})
27+
28+
// Populate mock Redis with some data
29+
ts.mockRedis.HSet("myhash", "key1", "value1", "key2", "value2", "key3", "value3")
30+
}
31+
32+
func (ts *TestSuite) TestHscanOnlyKeys() {
33+
ctx := context.Background()
34+
35+
// Test HscanOnlyKeys
36+
scanCmd := HscanOnlyKeys(ctx, ts.client, "myhash", 0, "", 0)
37+
keys, cursor, err := scanCmd.Result()
38+
39+
ts.Nil(err)
40+
ts.Equal(uint64(0), cursor)
41+
ts.ElementsMatch([]string{"key1", "key2", "key3"}, keys)
42+
43+
// Test with match pattern
44+
scanCmd = HscanOnlyKeys(ctx, ts.client, "myhash", 0, "key[1-2]", 0)
45+
keys, cursor, err = scanCmd.Result()
46+
47+
ts.Nil(err)
48+
ts.Equal(uint64(0), cursor)
49+
ts.ElementsMatch([]string{"key1", "key2"}, keys)
50+
}
51+
52+
func (ts *TestSuite) TestHscanOnlyValues() {
53+
ctx := context.Background()
54+
55+
// Test HscanOnlyValues
56+
scanCmd := HscanOnlyValues(ctx, ts.client, "myhash", 0, "", 0)
57+
values, cursor, err := scanCmd.Result()
58+
59+
ts.Nil(err)
60+
ts.Equal(uint64(0), cursor)
61+
ts.ElementsMatch([]string{"value1", "value2", "value3"}, values)
62+
63+
// Test with match pattern
64+
scanCmd = HscanOnlyValues(ctx, ts.client, "myhash", 0, "key[1-2]", 0)
65+
values, cursor, err = scanCmd.Result()
66+
67+
ts.Nil(err)
68+
ts.Equal(uint64(0), cursor)
69+
ts.ElementsMatch([]string{"value1", "value2"}, values)
70+
}

pkg/client/redis/paginate.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package kredis
2+
3+
import (
4+
"errors"
5+
6+
"github.com/redis/go-redis/v9"
7+
)
8+
9+
func PaginateScanCmd(scanFn func(cursor uint64) *redis.ScanCmd, callbackFn func(keys []string)) (err error) {
10+
var keys []string
11+
for cursor := uint64(0); ; {
12+
if keys, cursor, err = scanFn(cursor).Result(); err != nil {
13+
if errors.Is(err, redis.Nil) {
14+
err = nil
15+
}
16+
return err
17+
}
18+
19+
callbackFn(keys)
20+
21+
if cursor == 0 {
22+
return nil
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)