Skip to content

Commit aba21be

Browse files
authored
Add Redis Gears support (#2675)
* Add gears commands and tests * Fix tfunctionlist and add docstrings * fixes * fixes * add tfcallasync to gearsCmdable
1 parent 558581e commit aba21be

File tree

5 files changed

+368
-1
lines changed

5 files changed

+368
-1
lines changed

.github/wordlist.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ uri
5252
URI
5353
url
5454
variadic
55-
RedisStack
55+
RedisStack
56+
RedisGears

command.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3713,6 +3713,71 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error {
37133713
return nil
37143714
}
37153715

3716+
//-----------------------------------------------------------------------
3717+
3718+
type MapStringInterfaceSliceCmd struct {
3719+
baseCmd
3720+
3721+
val []map[string]interface{}
3722+
}
3723+
3724+
var _ Cmder = (*MapStringInterfaceSliceCmd)(nil)
3725+
3726+
func NewMapStringInterfaceSliceCmd(ctx context.Context, args ...interface{}) *MapStringInterfaceSliceCmd {
3727+
return &MapStringInterfaceSliceCmd{
3728+
baseCmd: baseCmd{
3729+
ctx: ctx,
3730+
args: args,
3731+
},
3732+
}
3733+
}
3734+
3735+
func (cmd *MapStringInterfaceSliceCmd) SetVal(val []map[string]interface{}) {
3736+
cmd.val = val
3737+
}
3738+
3739+
func (cmd *MapStringInterfaceSliceCmd) Val() []map[string]interface{} {
3740+
return cmd.val
3741+
}
3742+
3743+
func (cmd *MapStringInterfaceSliceCmd) Result() ([]map[string]interface{}, error) {
3744+
return cmd.Val(), cmd.Err()
3745+
}
3746+
3747+
func (cmd *MapStringInterfaceSliceCmd) String() string {
3748+
return cmdString(cmd, cmd.val)
3749+
}
3750+
3751+
func (cmd *MapStringInterfaceSliceCmd) readReply(rd *proto.Reader) error {
3752+
n, err := rd.ReadArrayLen()
3753+
if err != nil {
3754+
return err
3755+
}
3756+
3757+
cmd.val = make([]map[string]interface{}, n)
3758+
for i := 0; i < n; i++ {
3759+
nn, err := rd.ReadMapLen()
3760+
if err != nil {
3761+
return err
3762+
}
3763+
cmd.val[i] = make(map[string]interface{}, nn)
3764+
for f := 0; f < nn; f++ {
3765+
k, err := rd.ReadString()
3766+
if err != nil {
3767+
return err
3768+
}
3769+
v, err := rd.ReadReply()
3770+
if err != nil {
3771+
if err != Nil {
3772+
return err
3773+
}
3774+
}
3775+
cmd.val[i][k] = v
3776+
}
3777+
}
3778+
return nil
3779+
}
3780+
37163781
//------------------------------------------------------------------------------
37173782

37183783
type KeyValuesCmd struct {

commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ type Cmdable interface {
505505

506506
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
507507

508+
gearsCmdable
508509
probabilisticCmdable
509510
}
510511

redis_gears.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package redis
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
type gearsCmdable interface {
10+
TFunctionLoad(ctx context.Context, lib string) *StatusCmd
11+
TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd
12+
TFunctionDelete(ctx context.Context, libName string) *StatusCmd
13+
TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd
14+
TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd
15+
TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
16+
TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
17+
TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
18+
TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
19+
}
20+
type TFunctionLoadOptions struct {
21+
Replace bool
22+
Config string
23+
}
24+
25+
type TFunctionListOptions struct {
26+
Withcode bool
27+
Verbose int
28+
Library string
29+
}
30+
31+
type TFCallOptions struct {
32+
Keys []string
33+
Arguments []string
34+
}
35+
36+
// TFunctionLoad - load a new JavaScript library into Redis.
37+
// For more information - https://redis.io/commands/tfunction-load/
38+
func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd {
39+
args := []interface{}{"TFUNCTION", "LOAD", lib}
40+
cmd := NewStatusCmd(ctx, args...)
41+
_ = c(ctx, cmd)
42+
return cmd
43+
}
44+
45+
func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd {
46+
args := []interface{}{"TFUNCTION", "LOAD"}
47+
if options != nil {
48+
if options.Replace {
49+
args = append(args, "REPLACE")
50+
}
51+
if options.Config != "" {
52+
args = append(args, "CONFIG", options.Config)
53+
}
54+
}
55+
args = append(args, lib)
56+
cmd := NewStatusCmd(ctx, args...)
57+
_ = c(ctx, cmd)
58+
return cmd
59+
}
60+
61+
// TFunctionDelete - delete a JavaScript library from Redis.
62+
// For more information - https://redis.io/commands/tfunction-delete/
63+
func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd {
64+
args := []interface{}{"TFUNCTION", "DELETE", libName}
65+
cmd := NewStatusCmd(ctx, args...)
66+
_ = c(ctx, cmd)
67+
return cmd
68+
}
69+
70+
// TFunctionList - list the functions with additional information about each function.
71+
// For more information - https://redis.io/commands/tfunction-list/
72+
func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd {
73+
args := []interface{}{"TFUNCTION", "LIST"}
74+
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
75+
_ = c(ctx, cmd)
76+
return cmd
77+
}
78+
79+
func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd {
80+
args := []interface{}{"TFUNCTION", "LIST"}
81+
if options != nil {
82+
if options.Withcode {
83+
args = append(args, "WITHCODE")
84+
}
85+
if options.Verbose != 0 {
86+
v := strings.Repeat("v", options.Verbose)
87+
args = append(args, v)
88+
}
89+
if options.Library != "" {
90+
args = append(args, "LIBRARY", options.Library)
91+
92+
}
93+
}
94+
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
95+
_ = c(ctx, cmd)
96+
return cmd
97+
}
98+
99+
// TFCall - invoke a function.
100+
// For more information - https://redis.io/commands/tfcall/
101+
func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
102+
lf := libName + "." + funcName
103+
args := []interface{}{"TFCALL", lf, numKeys}
104+
cmd := NewCmd(ctx, args...)
105+
_ = c(ctx, cmd)
106+
return cmd
107+
}
108+
109+
func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
110+
lf := libName + "." + funcName
111+
args := []interface{}{"TFCALL", lf, numKeys}
112+
if options != nil {
113+
if options.Keys != nil {
114+
for _, key := range options.Keys {
115+
116+
args = append(args, key)
117+
}
118+
}
119+
if options.Arguments != nil {
120+
for _, key := range options.Arguments {
121+
122+
args = append(args, key)
123+
}
124+
}
125+
}
126+
cmd := NewCmd(ctx, args...)
127+
_ = c(ctx, cmd)
128+
return cmd
129+
}
130+
131+
// TFCallASYNC - invoke an asynchronous JavaScript function (coroutine).
132+
// For more information - https://redis.io/commands/TFCallASYNC/
133+
func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
134+
lf := fmt.Sprintf("%s.%s", libName, funcName)
135+
args := []interface{}{"TFCALLASYNC", lf, numKeys}
136+
cmd := NewCmd(ctx, args...)
137+
_ = c(ctx, cmd)
138+
return cmd
139+
}
140+
141+
func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
142+
lf := fmt.Sprintf("%s.%s", libName, funcName)
143+
args := []interface{}{"TFCALLASYNC", lf, numKeys}
144+
if options != nil {
145+
if options.Keys != nil {
146+
for _, key := range options.Keys {
147+
148+
args = append(args, key)
149+
}
150+
}
151+
if options.Arguments != nil {
152+
for _, key := range options.Arguments {
153+
154+
args = append(args, key)
155+
}
156+
}
157+
}
158+
cmd := NewCmd(ctx, args...)
159+
_ = c(ctx, cmd)
160+
return cmd
161+
}

redis_gears_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package redis_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
. "github.com/bsm/ginkgo/v2"
8+
. "github.com/bsm/gomega"
9+
"github.com/redis/go-redis/v9"
10+
)
11+
12+
func libCode(libName string) string {
13+
return fmt.Sprintf("#!js api_version=1.0 name=%s\n redis.registerFunction('foo', ()=>{{return 'bar'}})", libName)
14+
}
15+
16+
func libCodeWithConfig(libName string) string {
17+
lib := `#!js api_version=1.0 name=%s
18+
19+
var last_update_field_name = "__last_update__"
20+
21+
if (redis.config.last_update_field_name !== undefined) {
22+
if (typeof redis.config.last_update_field_name != 'string') {
23+
throw "last_update_field_name must be a string";
24+
}
25+
last_update_field_name = redis.config.last_update_field_name
26+
}
27+
28+
redis.registerFunction("hset", function(client, key, field, val){
29+
// get the current time in ms
30+
var curr_time = client.call("time")[0];
31+
return client.call('hset', key, field, val, last_update_field_name, curr_time);
32+
});`
33+
return fmt.Sprintf(lib, libName)
34+
}
35+
36+
var _ = Describe("RedisGears commands", Label("gears"), func() {
37+
ctx := context.TODO()
38+
var client *redis.Client
39+
40+
BeforeEach(func() {
41+
client = redis.NewClient(&redis.Options{Addr: ":6379"})
42+
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
43+
})
44+
45+
AfterEach(func() {
46+
Expect(client.Close()).NotTo(HaveOccurred())
47+
})
48+
49+
It("should TFunctionLoad, TFunctionLoadArgs and TFunctionDelete ", Label("gears", "tfunctionload"), func() {
50+
51+
resultAdd, err := client.TFunctionLoad(ctx, libCode("libtflo1")).Result()
52+
Expect(err).NotTo(HaveOccurred())
53+
Expect(resultAdd).To(BeEquivalentTo("OK"))
54+
opt := &redis.TFunctionLoadOptions{Replace: true, Config: `{"last_update_field_name":"last_update"}`}
55+
resultAdd, err = client.TFunctionLoadArgs(ctx, libCodeWithConfig("libtflo1"), opt).Result()
56+
Expect(err).NotTo(HaveOccurred())
57+
Expect(resultAdd).To(BeEquivalentTo("OK"))
58+
resultAdd, err = client.TFunctionDelete(ctx, "libtflo1").Result()
59+
Expect(err).NotTo(HaveOccurred())
60+
Expect(resultAdd).To(BeEquivalentTo("OK"))
61+
62+
})
63+
It("should TFunctionList", Label("gears", "tfunctionlist"), func() {
64+
resultAdd, err := client.TFunctionLoad(ctx, libCode("libtfli1")).Result()
65+
Expect(err).NotTo(HaveOccurred())
66+
Expect(resultAdd).To(BeEquivalentTo("OK"))
67+
resultAdd, err = client.TFunctionLoad(ctx, libCode("libtfli2")).Result()
68+
Expect(err).NotTo(HaveOccurred())
69+
Expect(resultAdd).To(BeEquivalentTo("OK"))
70+
resultList, err := client.TFunctionList(ctx).Result()
71+
Expect(err).NotTo(HaveOccurred())
72+
Expect(resultList[0]["engine"]).To(BeEquivalentTo("js"))
73+
opt := &redis.TFunctionListOptions{Withcode: true, Verbose: 2}
74+
resultListArgs, err := client.TFunctionListArgs(ctx, opt).Result()
75+
Expect(err).NotTo(HaveOccurred())
76+
Expect(resultListArgs[0]["code"]).To(BeEquivalentTo(libCode("libtfli1")))
77+
resultAdd, err = client.TFunctionDelete(ctx, "libtfli1").Result()
78+
Expect(err).NotTo(HaveOccurred())
79+
Expect(resultAdd).To(BeEquivalentTo("OK"))
80+
resultAdd, err = client.TFunctionDelete(ctx, "libtfli2").Result()
81+
Expect(err).NotTo(HaveOccurred())
82+
Expect(resultAdd).To(BeEquivalentTo("OK"))
83+
})
84+
85+
It("should TFCall", Label("gears", "tfcall"), func() {
86+
var resultAdd interface{}
87+
resultAdd, err := client.TFunctionLoad(ctx, libCode("libtfc1")).Result()
88+
Expect(err).NotTo(HaveOccurred())
89+
Expect(resultAdd).To(BeEquivalentTo("OK"))
90+
resultAdd, err = client.TFCall(ctx, "libtfc1", "foo", 0).Result()
91+
Expect(err).NotTo(HaveOccurred())
92+
Expect(resultAdd).To(BeEquivalentTo("bar"))
93+
resultAdd, err = client.TFunctionDelete(ctx, "libtfc1").Result()
94+
Expect(err).NotTo(HaveOccurred())
95+
Expect(resultAdd).To(BeEquivalentTo("OK"))
96+
})
97+
98+
It("should TFCallArgs", Label("gears", "tfcallargs"), func() {
99+
var resultAdd interface{}
100+
resultAdd, err := client.TFunctionLoad(ctx, libCode("libtfca1")).Result()
101+
Expect(err).NotTo(HaveOccurred())
102+
Expect(resultAdd).To(BeEquivalentTo("OK"))
103+
opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}}
104+
resultAdd, err = client.TFCallArgs(ctx, "libtfca1", "foo", 0, opt).Result()
105+
Expect(err).NotTo(HaveOccurred())
106+
Expect(resultAdd).To(BeEquivalentTo("bar"))
107+
resultAdd, err = client.TFunctionDelete(ctx, "libtfca1").Result()
108+
Expect(err).NotTo(HaveOccurred())
109+
Expect(resultAdd).To(BeEquivalentTo("OK"))
110+
})
111+
112+
It("should TFCallASYNC", Label("gears", "TFCallASYNC"), func() {
113+
var resultAdd interface{}
114+
resultAdd, err := client.TFunctionLoad(ctx, libCode("libtfc1")).Result()
115+
Expect(err).NotTo(HaveOccurred())
116+
Expect(resultAdd).To(BeEquivalentTo("OK"))
117+
resultAdd, err = client.TFCallASYNC(ctx, "libtfc1", "foo", 0).Result()
118+
Expect(err).NotTo(HaveOccurred())
119+
Expect(resultAdd).To(BeEquivalentTo("bar"))
120+
resultAdd, err = client.TFunctionDelete(ctx, "libtfc1").Result()
121+
Expect(err).NotTo(HaveOccurred())
122+
Expect(resultAdd).To(BeEquivalentTo("OK"))
123+
})
124+
125+
It("should TFCallASYNCArgs", Label("gears", "TFCallASYNCargs"), func() {
126+
var resultAdd interface{}
127+
resultAdd, err := client.TFunctionLoad(ctx, libCode("libtfca1")).Result()
128+
Expect(err).NotTo(HaveOccurred())
129+
Expect(resultAdd).To(BeEquivalentTo("OK"))
130+
opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}}
131+
resultAdd, err = client.TFCallASYNCArgs(ctx, "libtfca1", "foo", 0, opt).Result()
132+
Expect(err).NotTo(HaveOccurred())
133+
Expect(resultAdd).To(BeEquivalentTo("bar"))
134+
resultAdd, err = client.TFunctionDelete(ctx, "libtfca1").Result()
135+
Expect(err).NotTo(HaveOccurred())
136+
Expect(resultAdd).To(BeEquivalentTo("OK"))
137+
})
138+
139+
})

0 commit comments

Comments
 (0)