From a9b4c5cfe638df9fba2ca9cf00252f74e0f1f6dd Mon Sep 17 00:00:00 2001 From: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:59:27 +0300 Subject: [PATCH 1/2] Support Hash-field expiration commands (#2991) * Add HExpire command * Add HPExpire, HexpireAt, HPExpireAt, HTTL, HPTTL, HPersist,HExpireTime, HPExpireTIme, HGetF, HSetF commands * add docstring * add tests and fix commands * modify commands * api changes * fix tests * remove tests from RE --- commands_test.go | 148 ++++++++++++++++++++++++++++ hash_commands.go | 250 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 397 insertions(+), 1 deletion(-) diff --git a/commands_test.go b/commands_test.go index d30a9d8bb..b0224449b 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2430,6 +2430,154 @@ var _ = Describe("Commands", func() { Equal([]redis.KeyValue{{Key: "key2", Value: "hello2"}}), )) }) + + It("should HExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { + res, err := client.HExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err = client.HExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { + _, err := client.HPExpire(ctx, "no_such_key", 10, "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HPExpire(ctx, "myhash", 10, "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpireAt(ctx, "myhash", time.Now().Add(10*time.Second), "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HPExpireAt", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPExpireAt(ctx, "no_such_key", time.Now().Add(10*time.Second), "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HPExpireAt(ctx, "myhash", time.Now().Add(10*time.Second), "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, 1, -2})) + }) + + It("should HPersist", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPersist(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HPersist(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{-1, -1, -2})) + + res, err = client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HPersist(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -1, -2})) + }) + + It("should HExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HExpireTime(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(BeNumerically("~", time.Now().Add(10*time.Second).Unix(), 1)) + }) + + It("should HPExpireTime", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPExpireTime(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HPExpireTime(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo([]int64{time.Now().Add(10 * time.Second).UnixMilli(), -1, -2})) + }) + + It("should HTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HTTL(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{10, -1, -2})) + }) + + It("should HPTTL", Label("hash-expiration", "NonRedisEnterprise"), func() { + + _, err := client.HPTTL(ctx, "no_such_key", "field1", "field2", "field3").Result() + Expect(err).To(BeNil()) + for i := 0; i < 100; i++ { + sadd := client.HSet(ctx, "myhash", fmt.Sprintf("key%d", i), "hello") + Expect(sadd.Err()).NotTo(HaveOccurred()) + } + + res, err := client.HExpire(ctx, "myhash", 10, "key1", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]int64{1, -2})) + + res, err = client.HPTTL(ctx, "myhash", "key1", "key2", "key200").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1)) + }) }) Describe("hyperloglog", func() { diff --git a/hash_commands.go b/hash_commands.go index 2c62a75ad..59fbb8656 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -1,6 +1,9 @@ package redis -import "context" +import ( + "context" + "time" +) type HashCmdable interface { HDel(ctx context.Context, key string, fields ...string) *IntCmd @@ -172,3 +175,248 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str _ = c(ctx, cmd) return cmd } + +type HExpireArgs struct { + NX bool + XX bool + GT bool + LT bool +} + +// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. +// For more information - https://redis.io/commands/hexpire/ +func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIRE", key, expiration, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. +// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. +// For more information - https://redis.io/commands/hexpire/ +func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIRE", key, expiration} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds. +// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields. +// The command modifies the standard time.Duration to milliseconds for the Redis command. +// For more information - https://redis.io/commands/hpexpire/ +func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds. +// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields. +// The command sets absolute expiration times based on the UNIX timestamp provided. +// For more information - https://redis.io/commands/hexpireat/ +func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd { + + args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIREAT", key, tm.Unix()} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds. +// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds. +// For more information - https://redis.io/commands/hpexpireat/ +func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond)} + + // only if one argument is true, we can add it to the args + // if more than one argument is true, it will cause an error + if expirationArgs.NX { + args = append(args, "NX") + } else if expirationArgs.XX { + args = append(args, "XX") + } else if expirationArgs.GT { + args = append(args, "GT") + } else if expirationArgs.LT { + args = append(args, "LT") + } + + args = append(args, "FIELDS", len(fields)) + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPersist - Removes the expiration time from specified fields in a hash. +// Accepts a key and the fields themselves. +// This command ensures that each field specified will have its expiration removed if present. +// For more information - https://redis.io/commands/hpersist/ +func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds. +// Requires a key and the fields themselves to fetch their expiration timestamps. +// This command returns the expiration times for each field or error/status codes for each field as specified. +// For more information - https://redis.io/commands/hexpiretime/ +func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds. +// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters. +// Provides the expiration timestamp for each field in milliseconds. +// For more information - https://redis.io/commands/hexpiretime/ +func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds. +// Requires a key and the fields themselves. It returns the TTL for each specified field. +// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate. +// For more information - https://redis.io/commands/httl/ +func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HTTL", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds. +// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields. +// This command provides the TTL in milliseconds for each field or returns error/status codes as needed. +// For more information - https://redis.io/commands/hpttl/ +func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd { + args := []interface{}{"HPTTL", key, "FIELDS", len(fields)} + + for _, field := range fields { + args = append(args, field) + } + cmd := NewIntSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} From 0777247baa21ae915f02bdcd314d68c93b6b1e88 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Thu, 20 Jun 2024 01:00:09 +0300 Subject: [PATCH 2/2] Added test case for CLIENT KILL with MAXAGE option (#2971) * Added test case for CLIENT KILL with MAXAGE option * Fixed sleep value * Added additional condition to kill specific connection * Test commit * Test commit * Updated test case to handle timeouts --------- Co-authored-by: ofekshenawa <104765379+ofekshenawa@users.noreply.github.com> --- commands_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/commands_test.go b/commands_test.go index b0224449b..5d7880434 100644 --- a/commands_test.go +++ b/commands_test.go @@ -193,6 +193,40 @@ var _ = Describe("Commands", func() { Expect(r.Val()).To(Equal(int64(0))) }) + It("should ClientKillByFilter with MAXAGE", func() { + var s []string + started := make(chan bool) + done := make(chan bool) + + go func() { + defer GinkgoRecover() + + started <- true + blpop := client.BLPop(ctx, 0, "list") + Expect(blpop.Val()).To(Equal(s)) + done <- true + }() + <-started + + select { + case <-done: + Fail("BLPOP is not blocked.") + case <-time.After(2 * time.Second): + // ok + } + + killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") + Expect(killed.Err()).NotTo(HaveOccurred()) + Expect(killed.Val()).To(Equal(int64(2))) + + select { + case <-done: + // ok + case <-time.After(time.Second): + Fail("BLPOP is still blocked.") + } + }) + It("should ClientID", func() { err := client.ClientID(ctx).Err() Expect(err).NotTo(HaveOccurred())