From cb1be727e352456e9cc63f849477c8ac408738c4 Mon Sep 17 00:00:00 2001 From: monkey Date: Fri, 26 Mar 2021 17:13:22 +0800 Subject: [PATCH 1/3] add new cmd: getEX getDel Signed-off-by: monkey --- commands.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ commands_test.go | 33 +++++++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/commands.go b/commands.go index b0ed4552..a0e1edb0 100644 --- a/commands.go +++ b/commands.go @@ -117,6 +117,8 @@ type Cmdable interface { Get(ctx context.Context, key string) *StringCmd GetRange(ctx context.Context, key string, start, end int64) *StringCmd GetSet(ctx context.Context, key string, value interface{}) *StringCmd + GetEX(ctx context.Context, key string, ttl *SetTTL) *StringCmd + GetDel(ctx context.Context, key string) *StringCmd Incr(ctx context.Context, key string) *IntCmd IncrBy(ctx context.Context, key string, value int64) *IntCmd IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd @@ -361,6 +363,58 @@ type statefulCmdable func(ctx context.Context, cmd Cmder) error //------------------------------------------------------------------------------ +type ttlAttr int + +const ( + TExpire ttlAttr = 1 << iota + TExpireAT + TKeepTTL + TPersist +) + +// TTL related parameters, not all commands support all ttl attributes. +// priority: Expire > ExpireAt > KeepTTL > Persist +type SetTTL struct { + // set the specified expire time. + // Expire > time.Second AND Expire % time.Second == 0: set key EX Expire/time.Second + // Expire < time.Second OR Expire % time.Second != 0: set key PX Expire/time.Millisecond + Expire time.Duration + + // set the specified Unix time at which the key will expire. + // Example: set key EXAT ExpireAt.Unix() + // Don't consider milliseconds for now(PXAT) + ExpireAt time.Time + + // Retain the time to live associated with the key. + KeepTTL bool + + // Remove the time to live associated with the key, Change to never expire + Persist bool +} + +func appendTTL(ctx context.Context, args []interface{}, t *SetTTL, attr ttlAttr) []interface{} { + if t == nil { + return args + } + + switch { + case attr&TExpire == 1 && t.Expire > 0: + if usePrecise(t.Expire) { + args = append(args, "px", formatMs(ctx, t.Expire)) + } else { + args = append(args, "ex", formatSec(ctx, t.Expire)) + } + case attr&TExpireAT == 1 && !t.ExpireAt.IsZero(): + args = append(args, "exat", t.ExpireAt.Unix()) + case attr&TKeepTTL == 1 && t.KeepTTL: + args = append(args, "keepttl") + case attr&TPersist == 1 && t.Persist: + args = append(args, "persist") + } + + return args +} + func (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd { cmd := NewStatusCmd(ctx, "auth", password) _ = c(ctx, cmd) @@ -710,6 +764,23 @@ func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *Str return cmd } +// redis-server version >= 6.2.0 +func (c cmdable) GetEX(ctx context.Context, key string, ttl *SetTTL) *StringCmd { + args := make([]interface{}, 2, 4) + args = append(args, "getex", key) + args = appendTTL(ctx, args, ttl, TExpire|TExpireAT|TPersist) + cmd := NewStringCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// redis-server version >= 6.2.0 +func (c cmdable) GetDel(ctx context.Context, key string) *StringCmd { + cmd := NewStringCmd(ctx, "getdel", key) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) Incr(ctx context.Context, key string) *IntCmd { cmd := NewIntCmd(ctx, "incr", key) _ = c(ctx, cmd) diff --git a/commands_test.go b/commands_test.go index d7900fc6..14740127 100644 --- a/commands_test.go +++ b/commands_test.go @@ -477,7 +477,7 @@ var _ = Describe("Commands", func() { //if too much time (>1s) is used during command execution, it may also cause the test to fail. //so the ObjectIdleTime result should be <=now-start+1s //link: https://github.com/redis/redis/blob/5b48d900498c85bbf4772c1d466c214439888115/src/object.c#L1265-L1272 - Expect(idleTime.Val()).To(BeNumerically("<=", time.Now().Sub(start) + time.Second)) + Expect(idleTime.Val()).To(BeNumerically("<=", time.Now().Sub(start)+time.Second)) }) It("should Persist", func() { @@ -1083,6 +1083,37 @@ var _ = Describe("Commands", func() { Expect(get.Val()).To(Equal("0")) }) + It("should GetEX", func() { + set := client.Set(ctx, "key", "value", 100*time.Second) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + ttl := client.TTL(ctx, "key") + Expect(ttl.Err()).NotTo(HaveOccurred()) + Expect(ttl.Val()).To(BeNumerically("~", 100*time.Second, 3*time.Second)) + + getEX := client.GetEX(ctx, "key", &redis.SetTTL{Expire: 200 * time.Second}) + Expect(getEX.Err()).NotTo(HaveOccurred()) + Expect(getEX.Val()).To(Equal("value")) + + ttl = client.TTL(ctx, "key") + Expect(ttl.Err()).NotTo(HaveOccurred()) + Expect(ttl.Val()).To(BeNumerically("~", 200*time.Second, 3*time.Second)) + }) + + It("should GetDel", func() { + set := client.Set(ctx, "key", "value", 0) + Expect(set.Err()).NotTo(HaveOccurred()) + Expect(set.Val()).To(Equal("OK")) + + getDel := client.GetDel(ctx, "key") + Expect(getDel.Err()).NotTo(HaveOccurred()) + Expect(getDel.Val()).To(Equal("value")) + + get := client.Get(ctx, "key") + Expect(get.Err()).To(Equal(redis.Nil)) + }) + It("should Incr", func() { set := client.Set(ctx, "key", "10", 0) Expect(set.Err()).NotTo(HaveOccurred()) From e7dbdda439be14836a94c85d24f5f7f019f70f45 Mon Sep 17 00:00:00 2001 From: monkey Date: Sat, 27 Mar 2021 10:37:44 +0800 Subject: [PATCH 2/3] add HRANDFIELD and ZRANDMEMBER commands Signed-off-by: monkey --- commands.go | 121 +++++++++++++++++++++++------------------------ commands_test.go | 34 ++++++++++++- 2 files changed, 91 insertions(+), 64 deletions(-) diff --git a/commands.go b/commands.go index a0e1edb0..7dd78cfb 100644 --- a/commands.go +++ b/commands.go @@ -9,11 +9,18 @@ import ( "github.com/go-redis/redis/v8/internal" ) -// KeepTTL is an option for Set command to keep key's existing TTL. -// For example: -// -// rdb.Set(ctx, key, value, redis.KeepTTL) -const KeepTTL = -1 +const ( + // KeepTTL is an option for Set command to keep key's existing TTL. + // For example: + // + // rdb.Set(ctx, key, value, redis.KeepTTL) + KeepTTL = -1 + + // Persist is remove the time to live associated with the key. + // For example: + // rdb.GetEX(ctx, key, redis.Persist) + Persist = -2 +) func usePrecise(dur time.Duration) bool { return dur < time.Second || dur%time.Second != 0 @@ -117,7 +124,7 @@ type Cmdable interface { Get(ctx context.Context, key string) *StringCmd GetRange(ctx context.Context, key string, start, end int64) *StringCmd GetSet(ctx context.Context, key string, value interface{}) *StringCmd - GetEX(ctx context.Context, key string, ttl *SetTTL) *StringCmd + GetEX(ctx context.Context, key string, expiration time.Duration) *StringCmd GetDel(ctx context.Context, key string) *StringCmd Incr(ctx context.Context, key string) *IntCmd IncrBy(ctx context.Context, key string, value int64) *IntCmd @@ -162,6 +169,7 @@ type Cmdable interface { HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd HVals(ctx context.Context, key string) *StringSliceCmd + HRandField(ctx context.Context, key string, count int, withValues bool) *StringSliceCmd BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd @@ -265,6 +273,7 @@ type Cmdable interface { ZRevRank(ctx context.Context, key, member string) *IntCmd ZScore(ctx context.Context, key, member string) *FloatCmd ZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd + ZRandMember(ctx context.Context, key string, count int, withScores bool) *StringSliceCmd PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd PFCount(ctx context.Context, keys ...string) *IntCmd @@ -363,58 +372,6 @@ type statefulCmdable func(ctx context.Context, cmd Cmder) error //------------------------------------------------------------------------------ -type ttlAttr int - -const ( - TExpire ttlAttr = 1 << iota - TExpireAT - TKeepTTL - TPersist -) - -// TTL related parameters, not all commands support all ttl attributes. -// priority: Expire > ExpireAt > KeepTTL > Persist -type SetTTL struct { - // set the specified expire time. - // Expire > time.Second AND Expire % time.Second == 0: set key EX Expire/time.Second - // Expire < time.Second OR Expire % time.Second != 0: set key PX Expire/time.Millisecond - Expire time.Duration - - // set the specified Unix time at which the key will expire. - // Example: set key EXAT ExpireAt.Unix() - // Don't consider milliseconds for now(PXAT) - ExpireAt time.Time - - // Retain the time to live associated with the key. - KeepTTL bool - - // Remove the time to live associated with the key, Change to never expire - Persist bool -} - -func appendTTL(ctx context.Context, args []interface{}, t *SetTTL, attr ttlAttr) []interface{} { - if t == nil { - return args - } - - switch { - case attr&TExpire == 1 && t.Expire > 0: - if usePrecise(t.Expire) { - args = append(args, "px", formatMs(ctx, t.Expire)) - } else { - args = append(args, "ex", formatSec(ctx, t.Expire)) - } - case attr&TExpireAT == 1 && !t.ExpireAt.IsZero(): - args = append(args, "exat", t.ExpireAt.Unix()) - case attr&TKeepTTL == 1 && t.KeepTTL: - args = append(args, "keepttl") - case attr&TPersist == 1 && t.Persist: - args = append(args, "persist") - } - - return args -} - func (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd { cmd := NewStatusCmd(ctx, "auth", password) _ = c(ctx, cmd) @@ -764,17 +721,29 @@ func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *Str return cmd } -// redis-server version >= 6.2.0 -func (c cmdable) GetEX(ctx context.Context, key string, ttl *SetTTL) *StringCmd { - args := make([]interface{}, 2, 4) +// redis-server version >= 6.2.0. +// +// A value of zero means that the expiration time will not be changed. +// Persist(-2) Remove the time to live associated with the key. +func (c cmdable) GetEX(ctx context.Context, key string, expiration time.Duration) *StringCmd { + args := make([]interface{}, 0, 4) args = append(args, "getex", key) - args = appendTTL(ctx, args, ttl, TExpire|TExpireAT|TPersist) + if expiration > 0 { + if usePrecise(expiration) { + args = append(args, "px", formatMs(ctx, expiration)) + } else { + args = append(args, "ex", formatSec(ctx, expiration)) + } + } else if expiration == Persist { + args = append(args, "persist") + } + cmd := NewStringCmd(ctx, args...) _ = c(ctx, cmd) return cmd } -// redis-server version >= 6.2.0 +// redis-server version >= 6.2.0. func (c cmdable) GetDel(ctx context.Context, key string) *StringCmd { cmd := NewStringCmd(ctx, "getdel", key) _ = c(ctx, cmd) @@ -1253,6 +1222,20 @@ func (c cmdable) HVals(ctx context.Context, key string) *StringSliceCmd { return cmd } +func (c cmdable) HRandField(ctx context.Context, key string, count int, withValues bool) *StringSliceCmd { + args := make([]interface{}, 0, 4) + + // Although count=0 is meaningless, redis accepts count=0. + args = append(args, "hrandfield", key, count) + if withValues { + args = append(args, "withvalues") + } + + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + //------------------------------------------------------------------------------ func (c cmdable) BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd { @@ -2327,6 +2310,18 @@ func (c cmdable) ZUnionStore(ctx context.Context, dest string, store *ZStore) *I return cmd } +func (c cmdable) ZRandMember(ctx context.Context, key string, count int, withScores bool) *StringSliceCmd { + args := make([]interface{}, 0, 4) + args = append(args, "zrandmember", key, count) + if withScores { + args = append(args, "withscores") + } + + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + //------------------------------------------------------------------------------ func (c cmdable) PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd { diff --git a/commands_test.go b/commands_test.go index 14740127..a261d788 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1092,7 +1092,7 @@ var _ = Describe("Commands", func() { Expect(ttl.Err()).NotTo(HaveOccurred()) Expect(ttl.Val()).To(BeNumerically("~", 100*time.Second, 3*time.Second)) - getEX := client.GetEX(ctx, "key", &redis.SetTTL{Expire: 200 * time.Second}) + getEX := client.GetEX(ctx, "key", 200*time.Second) Expect(getEX.Err()).NotTo(HaveOccurred()) Expect(getEX.Val()).To(Equal("value")) @@ -1832,6 +1832,22 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(slice).To(Equal([]string{"hello1", "hello2"})) }) + + It("should HRandField", func() { + err := client.HSet(ctx, "hash", "key1", "hello1").Err() + Expect(err).NotTo(HaveOccurred()) + err = client.HSet(ctx, "hash", "key2", "hello2").Err() + Expect(err).NotTo(HaveOccurred()) + + v := client.HRandField(ctx, "hash", 1, false) + Expect(v.Err()).NotTo(HaveOccurred()) + Expect(v.Result()).To(Or(Equal([]string{"key1"}), Equal([]string{"key2"}))) + + var slice []string + err = client.HRandField(ctx, "hash", 1, true).ScanSlice(&slice) + Expect(err).NotTo(HaveOccurred()) + Expect(slice).To(Or(Equal([]string{"key1", "hello1"}), Equal([]string{"key2", "hello2"}))) + }) }) Describe("hyperloglog", func() { @@ -3870,6 +3886,22 @@ var _ = Describe("Commands", func() { Member: "two", }})) }) + + It("should ZRandMember", func() { + err := client.ZAdd(ctx, "zset", &redis.Z{Score: 1, Member: "one"}).Err() + Expect(err).NotTo(HaveOccurred()) + err = client.ZAdd(ctx, "zset", &redis.Z{Score: 2, Member: "two"}).Err() + Expect(err).NotTo(HaveOccurred()) + + v := client.ZRandMember(ctx, "zset", 1, false) + Expect(v.Err()).NotTo(HaveOccurred()) + Expect(v.Val()).To(Or(Equal([]string{"one"}), Equal([]string{"two"}))) + + var slice []string + err = client.ZRandMember(ctx, "zset", 1, true).ScanSlice(&slice) + Expect(err).NotTo(HaveOccurred()) + Expect(slice).To(Or(Equal([]string{"one", "1"}), Equal([]string{"two", "2"}))) + }) }) Describe("streams", func() { From e3ce4ea661798e0d0e036021640cde507fda343b Mon Sep 17 00:00:00 2001 From: monkey Date: Sat, 27 Mar 2021 17:49:55 +0800 Subject: [PATCH 3/3] remove PERSIST, add unit tests to ensure the stability of ZRandMember and HRandField. Signed-off-by: monkey --- commands.go | 27 +++++++++++---------------- commands_test.go | 10 +++++++++- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/commands.go b/commands.go index 7dd78cfb..0ffceb58 100644 --- a/commands.go +++ b/commands.go @@ -9,18 +9,11 @@ import ( "github.com/go-redis/redis/v8/internal" ) -const ( - // KeepTTL is an option for Set command to keep key's existing TTL. - // For example: - // - // rdb.Set(ctx, key, value, redis.KeepTTL) - KeepTTL = -1 - - // Persist is remove the time to live associated with the key. - // For example: - // rdb.GetEX(ctx, key, redis.Persist) - Persist = -2 -) +// KeepTTL is an option for Set command to keep key's existing TTL. +// For example: +// +// rdb.Set(ctx, key, value, redis.KeepTTL) +const KeepTTL = -1 func usePrecise(dur time.Duration) bool { return dur < time.Second || dur%time.Second != 0 @@ -722,9 +715,7 @@ func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *Str } // redis-server version >= 6.2.0. -// -// A value of zero means that the expiration time will not be changed. -// Persist(-2) Remove the time to live associated with the key. +// A expiration of zero remove the time to live associated with the key(GetEX key persist). func (c cmdable) GetEX(ctx context.Context, key string, expiration time.Duration) *StringCmd { args := make([]interface{}, 0, 4) args = append(args, "getex", key) @@ -734,7 +725,7 @@ func (c cmdable) GetEX(ctx context.Context, key string, expiration time.Duration } else { args = append(args, "ex", formatSec(ctx, expiration)) } - } else if expiration == Persist { + } else if expiration == 0 { args = append(args, "persist") } @@ -1222,6 +1213,7 @@ func (c cmdable) HVals(ctx context.Context, key string) *StringSliceCmd { return cmd } +// redis-server version >= 6.2.0. func (c cmdable) HRandField(ctx context.Context, key string, count int, withValues bool) *StringSliceCmd { args := make([]interface{}, 0, 4) @@ -2310,8 +2302,11 @@ func (c cmdable) ZUnionStore(ctx context.Context, dest string, store *ZStore) *I return cmd } +// redis-server version >= 6.2.0. func (c cmdable) ZRandMember(ctx context.Context, key string, count int, withScores bool) *StringSliceCmd { args := make([]interface{}, 0, 4) + + // Although count=0 is meaningless, redis accepts count=0. args = append(args, "zrandmember", key, count) if withScores { args = append(args, "withscores") diff --git a/commands_test.go b/commands_test.go index a261d788..f4a1973d 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1841,7 +1841,11 @@ var _ = Describe("Commands", func() { v := client.HRandField(ctx, "hash", 1, false) Expect(v.Err()).NotTo(HaveOccurred()) - Expect(v.Result()).To(Or(Equal([]string{"key1"}), Equal([]string{"key2"}))) + Expect(v.Val()).To(Or(Equal([]string{"key1"}), Equal([]string{"key2"}))) + + v = client.HRandField(ctx, "hash", 0, false) + Expect(v.Err()).NotTo(HaveOccurred()) + Expect(v.Val()).To(HaveLen(0)) var slice []string err = client.HRandField(ctx, "hash", 1, true).ScanSlice(&slice) @@ -3897,6 +3901,10 @@ var _ = Describe("Commands", func() { Expect(v.Err()).NotTo(HaveOccurred()) Expect(v.Val()).To(Or(Equal([]string{"one"}), Equal([]string{"two"}))) + v = client.ZRandMember(ctx, "zset", 0, false) + Expect(v.Err()).NotTo(HaveOccurred()) + Expect(v.Val()).To(HaveLen(0)) + var slice []string err = client.ZRandMember(ctx, "zset", 1, true).ScanSlice(&slice) Expect(err).NotTo(HaveOccurred())