From 47eba53b4bf82e57b268f048fd083519bb568b8a Mon Sep 17 00:00:00 2001 From: wszaranski <67064139+wszaranski@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:47:22 +0100 Subject: [PATCH] Add EXPIRETIME command (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add EXPIRETIME command Redis documentation: https://redis.io/commands/expiretime/ Closes: https://github.com/alicebob/miniredis/issues/294 Signed-off-by: Wojciech Szarański * two more testcases --------- Signed-off-by: Wojciech Szarański Co-authored-by: Wojciech Szarański Co-authored-by: Harmen --- cmd_generic.go | 45 ++++++++++++++++++++++++++++++++++++++ cmd_generic_test.go | 26 ++++++++++++++++++++++ integration/string_test.go | 8 +++++++ 3 files changed, 79 insertions(+) diff --git a/cmd_generic.go b/cmd_generic.go index 009a0e40..f6de0373 100644 --- a/cmd_generic.go +++ b/cmd_generic.go @@ -12,6 +12,13 @@ import ( "github.com/alicebob/miniredis/v2/server" ) +const ( + // expiretimeReplyNoExpiration is returned by [Miniredis.cmdExpireTime] 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 = -2 +) + // commandsGeneric handles EXPIRE, TTL, PERSIST, &c. func commandsGeneric(m *Miniredis) { m.srv.Register("COPY", m.cmdCopy) @@ -20,6 +27,7 @@ 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("KEYS", m.cmdKeys) // MIGRATE m.srv.Register("MOVE", m.cmdMove) @@ -145,6 +153,43 @@ 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]. +// +// [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) + return + } + + ttl, ok := db.ttl[key] + if !ok { + c.WriteInt(expiretimeReplyNoExpiration) + return + } + + c.WriteInt(int(m.effectiveNow().Add(ttl).Unix())) + }) +} + // TOUCH func (m *Miniredis) cmdTouch(c *server.Peer, cmd string, args []string) { if !m.handleAuth(c) { diff --git a/cmd_generic_test.go b/cmd_generic_test.go index b7f29146..af1c3a09 100644 --- a/cmd_generic_test.go +++ b/cmd_generic_test.go @@ -362,6 +362,32 @@ func TestType(t *testing.T) { }) } +func TestExpireTime(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, "EXPIRETIME", "nosuch", proto.Int(-2)) + }) + + t.Run("noexpire", func(t *testing.T) { + s.Set("noexpire", "") + mustDo(t, c, "EXPIRETIME", "noexpire", proto.Int(-1)) + }) + + t.Run("", func(t *testing.T) { + s.Set("foo", "") + must1(t, c, "EXPIREAT", "foo", "10413792000") // Mon Jan 01 2300 00:00:00 GMT+0000 + mustDo(t, c, "EXPIRETIME", "foo", + proto.Int(10413792000), + ) + }) +} + func TestExists(t *testing.T) { s, err := Run() ok(t, err) diff --git a/integration/string_test.go b/integration/string_test.go index cfe27b4a..6501760f 100644 --- a/integration/string_test.go +++ b/integration/string_test.go @@ -167,13 +167,18 @@ func TestStringSetnx(t *testing.T) { func TestExpire(t *testing.T) { skip(t) testRaw(t, func(c *client) { + c.Do("EXPIRETIME", "missing") + c.Do("SET", "foo", "bar") + c.Do("EXPIRETIME", "foo") + c.Do("EXPIRE", "foo", "12") c.Do("TTL", "foo") c.Do("TTL", "nosuch") c.Do("SET", "foo", "bar") c.Do("PEXPIRE", "foo", "999999") c.Do("EXPIREAT", "foo", "2234567890") + c.Do("EXPIRETIME", "foo") c.Do("PEXPIREAT", "foo", "2234567890000") // c.Do("PTTL", "foo") c.Do("PTTL", "nosuch") @@ -221,6 +226,9 @@ func TestExpire(t *testing.T) { c.Error("wrong number", "PEXPIREAT") c.Error("wrong number", "PTTL") c.Error("wrong number", "PTTL", "too", "many") + + c.Error("wrong number", "EXPIRETIME") + c.Error("wrong number", "EXPIRETIME", "too", "many") }) }