From ce81a05fa2ef44f3ba4b9b5dd9fb790a69d91f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Szara=C5=84ski?= Date: Tue, 12 Mar 2024 11:14:55 +0100 Subject: [PATCH] Add PEXPIRETIME command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redis documentation: https://redis.io/commands/pexpiretime/ Signed-off-by: Wojciech SzaraƄski --- cmd_generic.go | 78 +++++++++++++++++++++++--------------- cmd_generic_test.go | 26 +++++++++++++ integration/string_test.go | 7 +++- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/cmd_generic.go b/cmd_generic.go index f6de037..f3fd604 100644 --- a/cmd_generic.go +++ b/cmd_generic.go @@ -13,12 +13,23 @@ import ( ) const ( - // expiretimeReplyNoExpiration is returned by [Miniredis.cmdExpireTime] if the key exists but has no associated expiration time + // expiretimeReplyNoExpiration is return value for EXPIRETIME and PEXPIRETIME if the key exists but has no associated expiration time expiretimeReplyNoExpiration = -1 - // expiretimeReplyMissingKey is returned by [Miniredis.cmdExpireTime] if the key does not exist + // expiretimeReplyMissingKey is return value for EXPIRETIME and PEXPIRETIME if the key does not exist expiretimeReplyMissingKey = -2 ) +func inSeconds(t time.Time) int { + return int(t.Unix()) +} + +func inMilliSeconds(t time.Time) int { + // Time.UnixMilli() was added in go 1.17 + // return int(t.UnixNano() / 1000000) is limited to dates between year 1678 and 2262 + // by using following calculation we extend this time without too much complexity + return int(t.Unix())*1000 + t.Nanosecond()/1000000 +} + // commandsGeneric handles EXPIRE, TTL, PERSIST, &c. func commandsGeneric(m *Miniredis) { m.srv.Register("COPY", m.cmdCopy) @@ -27,7 +38,8 @@ func commandsGeneric(m *Miniredis) { m.srv.Register("EXISTS", m.cmdExists) m.srv.Register("EXPIRE", makeCmdExpire(m, false, time.Second)) m.srv.Register("EXPIREAT", makeCmdExpire(m, true, time.Second)) - m.srv.Register("EXPIRETIME", m.cmdExpireTime) + m.srv.Register("EXPIRETIME", m.makeCmdExpireTime(inSeconds)) + m.srv.Register("PEXPIRETIME", m.makeCmdExpireTime(inMilliSeconds)) m.srv.Register("KEYS", m.cmdKeys) // MIGRATE m.srv.Register("MOVE", m.cmdMove) @@ -153,41 +165,45 @@ func makeCmdExpire(m *Miniredis, unix bool, d time.Duration) func(*server.Peer, } } -// cmdExpireTime returns the absolute Unix timestamp (since January 1, 1970) in seconds at which the given key will expire. -// See [redis documentation]. +// makeCmdExpireTime creates server command function that returns the absolute Unix timestamp (since January 1, 1970) +// at which the given key will expire, in unit selected by time result strategy (e.g. seconds, milliseconds). +// For more information see redis documentation for [expiretime] and [pexpiretime]. // -// [redis documentation]: https://redis.io/commands/expiretime/ -func (m *Miniredis) cmdExpireTime(c *server.Peer, cmd string, args []string) { - if len(args) != 1 { - setDirty(c) - c.WriteError(errWrongNumber(cmd)) - return - } - - if !m.handleAuth(c) { - return - } - if m.checkPubsub(c, cmd) { - return - } - - key := args[0] - withTx(m, c, func(c *server.Peer, ctx *connCtx) { - db := m.db(ctx.selectedDB) - - if _, ok := db.keys[key]; !ok { - c.WriteInt(expiretimeReplyMissingKey) +// [expiretime]: https://redis.io/commands/expiretime/ +// [pexpiretime]: https://redis.io/commands/pexpiretime/ +func (m *Miniredis) makeCmdExpireTime(timeResultStrategy func(time.Time) int) server.Cmd { + return func(c *server.Peer, cmd string, args []string) { + if len(args) != 1 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) return } - ttl, ok := db.ttl[key] - if !ok { - c.WriteInt(expiretimeReplyNoExpiration) + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { return } - c.WriteInt(int(m.effectiveNow().Add(ttl).Unix())) - }) + key := args[0] + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if _, ok := db.keys[key]; !ok { + c.WriteInt(expiretimeReplyMissingKey) + return + } + + ttl, ok := db.ttl[key] + if !ok { + c.WriteInt(expiretimeReplyNoExpiration) + return + } + + c.WriteInt(timeResultStrategy(m.effectiveNow().Add(ttl))) + }) + } } // TOUCH diff --git a/cmd_generic_test.go b/cmd_generic_test.go index af1c3a0..2f47b4d 100644 --- a/cmd_generic_test.go +++ b/cmd_generic_test.go @@ -388,6 +388,32 @@ func TestExpireTime(t *testing.T) { }) } +func TestPExpireTime(t *testing.T) { + s, err := Run() + ok(t, err) + defer s.Close() + c, err := proto.Dial(s.Addr()) + ok(t, err) + defer c.Close() + + t.Run("nosuch", func(t *testing.T) { + mustDo(t, c, "PEXPIRETIME", "nosuch", proto.Int(-2)) + }) + + t.Run("noexpire", func(t *testing.T) { + s.Set("noexpire", "") + mustDo(t, c, "PEXPIRETIME", "noexpire", proto.Int(-1)) + }) + + t.Run("", func(t *testing.T) { + s.Set("foo", "") + must1(t, c, "PEXPIREAT", "foo", "10413792000123") // Mon Jan 01 2300 00:00:00.123 GMT+0000 + mustDo(t, c, "PEXPIRETIME", "foo", + proto.Int(10413792000123), + ) + }) +} + func TestExists(t *testing.T) { s, err := Run() ok(t, err) diff --git a/integration/string_test.go b/integration/string_test.go index 6501760..c1877e5 100644 --- a/integration/string_test.go +++ b/integration/string_test.go @@ -168,9 +168,11 @@ func TestExpire(t *testing.T) { skip(t) testRaw(t, func(c *client) { c.Do("EXPIRETIME", "missing") + c.Do("PEXPIRETIME", "missing") c.Do("SET", "foo", "bar") c.Do("EXPIRETIME", "foo") + c.Do("PEXPIRETIME", "foo") c.Do("EXPIRE", "foo", "12") c.Do("TTL", "foo") @@ -178,8 +180,9 @@ func TestExpire(t *testing.T) { c.Do("SET", "foo", "bar") c.Do("PEXPIRE", "foo", "999999") c.Do("EXPIREAT", "foo", "2234567890") + c.Do("PEXPIREAT", "foo", "2234567890123") c.Do("EXPIRETIME", "foo") - c.Do("PEXPIREAT", "foo", "2234567890000") + c.Do("PEXPIRETIME", "foo") // c.Do("PTTL", "foo") c.Do("PTTL", "nosuch") @@ -229,6 +232,8 @@ func TestExpire(t *testing.T) { c.Error("wrong number", "EXPIRETIME") c.Error("wrong number", "EXPIRETIME", "too", "many") + c.Error("wrong number", "PEXPIRETIME") + c.Error("wrong number", "PEXPIRETIME", "too", "many") }) }