diff --git a/libs/common/ConvertUtils.cs b/libs/common/ConvertUtils.cs index 89ef9ec763..140d97ed36 100644 --- a/libs/common/ConvertUtils.cs +++ b/libs/common/ConvertUtils.cs @@ -70,5 +70,25 @@ public static long UnixTimestampInMillisecondsToTicks(long unixTimestamp) { return unixTimestamp * TimeSpan.TicksPerMillisecond + _unixEpochTicks; } + + /// + /// Convert ticks to Unix time in seconds. + /// + /// The ticks to convert. + /// The Unix time in seconds. + public static long UnixTimeInSecondsFromTicks(long ticks) + { + return ticks > 0 ? (ticks - _unixEpochTicks) / TimeSpan.TicksPerSecond : -1; + } + + /// + /// Convert ticks to Unix time in milliseconds. + /// + /// The ticks to convert. + /// The Unix time in milliseconds. + public static long UnixTimeInMillisecondsFromTicks(long ticks) + { + return ticks > 0 ? (ticks - _unixEpochTicks) / TimeSpan.TicksPerMillisecond : -1; + } } } \ No newline at end of file diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index da6f90165c..ac364ad74e 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -93,6 +93,18 @@ public GarnetStatus PTTL(ref SpanByte key, StoreType storeType, ref SpanByteAndM #endregion + #region EXPIRETIME + + /// + public GarnetStatus EXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output) + => storageSession.EXPIRETIME(ref key, storeType, ref output, ref context, ref objectContext); + + /// + public GarnetStatus PEXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output) + => storageSession.EXPIRETIME(ref key, storeType, ref output, ref context, ref objectContext, milliseconds: true); + + #endregion + #region SET /// public GarnetStatus SET(ref SpanByte key, ref SpanByte value) diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index bbae63343a..8a4e6044b2 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -77,6 +77,24 @@ public GarnetStatus PTTL(ref SpanByte key, StoreType storeType, ref SpanByteAndM #endregion + #region EXPIRETIME + + /// + public GarnetStatus EXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output) + { + garnetApi.WATCH(new ArgSlice(ref key), storeType); + return garnetApi.EXPIRETIME(ref key, storeType, ref output); + } + + /// + public GarnetStatus PEXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output) + { + garnetApi.WATCH(new ArgSlice(ref key), storeType); + return garnetApi.PEXPIRETIME(ref key, storeType, ref output); + } + + #endregion + #region SortedSet Methods /// diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 5b5f2997c8..53af70fdb2 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -1085,6 +1085,28 @@ public interface IGarnetReadApi #endregion + #region EXPIRETIME + + /// + /// Returns the absolute Unix timestamp (since January 1, 1970) in seconds at which the given key will expire. + /// + /// The key to get the expiration time for. + /// The type of store to retrieve the key from. + /// The output containing the expiration time. + /// The status of the operation. + GarnetStatus EXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output); + + /// + /// Returns the absolute Unix timestamp (since January 1, 1970) in milliseconds at which the given key will expire. + /// + /// The key to get the expiration time for. + /// The type of store to retrieve the key from. + /// The output containing the expiration time. + /// The status of the operation. + GarnetStatus PEXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output); + + #endregion + #region SortedSet Methods /// diff --git a/libs/server/Objects/Types/GarnetObjectType.cs b/libs/server/Objects/Types/GarnetObjectType.cs index a5d7f3e596..0657ab059d 100644 --- a/libs/server/Objects/Types/GarnetObjectType.cs +++ b/libs/server/Objects/Types/GarnetObjectType.cs @@ -29,6 +29,16 @@ public enum GarnetObjectType : byte /// Set, + /// + /// Special type indicating EXPIRETIME command + /// + Expiretime = 0xf9, + + /// + /// Special type indicating PEXPIRETIME command + /// + PExpiretime = 0xfa, + /// /// Special type indicating PERSIST command /// diff --git a/libs/server/Resp/KeyAdminCommands.cs b/libs/server/Resp/KeyAdminCommands.cs index 029e2ca16b..4d2aaf6ddc 100644 --- a/libs/server/Resp/KeyAdminCommands.cs +++ b/libs/server/Resp/KeyAdminCommands.cs @@ -404,5 +404,41 @@ private bool NetworkTTL(RespCommand command, ref TGarnetApi storageA } return true; } + + /// + /// Get the absolute Unix timestamp at which the given key will expire. + /// + /// + /// either if the call is for EXPIRETIME or PEXPIRETIME command + /// + /// Returns the absolute Unix timestamp (since January 1, 1970) in seconds or milliseconds at which the given key will expire. + private bool NetworkEXPIRETIME(RespCommand command, ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + if (parseState.Count != 1) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.EXPIRETIME)); + } + + var sbKey = parseState.GetArgSliceByRef(0).SpanByte; + var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + var status = command == RespCommand.EXPIRETIME ? + storageApi.EXPIRETIME(ref sbKey, StoreType.All, ref o) : + storageApi.PEXPIRETIME(ref sbKey, StoreType.All, ref o); + + if (status == GarnetStatus.OK) + { + if (!o.IsSpanByte) + SendAndReset(o.Memory, o.Length); + else + dcurr += o.Length; + } + else + { + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_N2, ref dcurr, dend)) + SendAndReset(); + } + return true; + } } } \ No newline at end of file diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index efd8cf3df6..ce316987da 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -25,6 +25,7 @@ public enum RespCommand : byte COSCAN, DBSIZE, EXISTS, + EXPIRETIME, GEODIST, GEOHASH, GEOPOS, @@ -49,6 +50,7 @@ public enum RespCommand : byte LRANGE, MEMORY_USAGE, MGET, + PEXPIRETIME, PFCOUNT, PTTL, SCAN, @@ -1329,6 +1331,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SDIFFSTORE; } + else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read("10\r\nEXPI"u8) && *(uint*)(ptr + 9) == MemoryMarshal.Read("RETIME\r\n"u8)) + { + return RespCommand.EXPIRETIME; + } break; case 11: @@ -1356,6 +1362,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SINTERSTORE; } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nPEXPI"u8) && *(uint*)(ptr + 10) == MemoryMarshal.Read("RETIME\r\n"u8)) + { + return RespCommand.PEXPIRETIME; + } break; case 12: diff --git a/libs/server/Resp/RespCommandsInfo.json b/libs/server/Resp/RespCommandsInfo.json index b6c5265760..11527e027b 100644 --- a/libs/server/Resp/RespCommandsInfo.json +++ b/libs/server/Resp/RespCommandsInfo.json @@ -1623,6 +1623,35 @@ ], "SubCommands": null }, + { + "Command": "EXPIRETIME", + "Name": "EXPIRETIME", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, { "Command": "FAILOVER", "Name": "FAILOVER", @@ -3341,6 +3370,35 @@ ], "SubCommands": null }, + { + "Command": "PEXPIRETIME", + "Name": "PEXPIRETIME", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, { "Command": "PFADD", "Name": "PFADD", diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 848a0edb3d..2933508380 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -516,6 +516,8 @@ private bool ProcessBasicCommands(RespCommand cmd, ref TGarnetApi st RespCommand.EXISTS => NetworkEXISTS(ref storageApi), RespCommand.EXPIRE => NetworkEXPIRE(RespCommand.EXPIRE, ref storageApi), RespCommand.PEXPIRE => NetworkEXPIRE(RespCommand.PEXPIRE, ref storageApi), + RespCommand.EXPIRETIME => NetworkEXPIRETIME(RespCommand.EXPIRETIME, ref storageApi), + RespCommand.PEXPIRETIME => NetworkEXPIRETIME(RespCommand.PEXPIRETIME, ref storageApi), RespCommand.PERSIST => NetworkPERSIST(ref storageApi), RespCommand.GETRANGE => NetworkGetRange(ref storageApi), RespCommand.TTL => NetworkTTL(RespCommand.TTL, ref storageApi), diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 193a125f43..5eb0835968 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -217,6 +217,17 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd (start, end) = NormalizeRange(start, end, len); CopyRespTo(ref value, ref dst, start, end); return; + + case RespCommand.EXPIRETIME: + var expireTime = ConvertUtils.UnixTimeInSecondsFromTicks(value.MetadataSize > 0 ? value.ExtraMetadata : -1); + CopyRespNumber(expireTime, ref dst); + return; + + case RespCommand.PEXPIRETIME: + var pexpireTime = ConvertUtils.UnixTimeInMillisecondsFromTicks(value.MetadataSize > 0 ? value.ExtraMetadata : -1); + CopyRespNumber(pexpireTime, ref dst); + return; + default: throw new GarnetException("Unsupported operation on input"); } diff --git a/libs/server/Storage/Functions/ObjectStore/ReadMethods.cs b/libs/server/Storage/Functions/ObjectStore/ReadMethods.cs index 2cc20b0dc0..b360f7849a 100644 --- a/libs/server/Storage/Functions/ObjectStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/ObjectStore/ReadMethods.cs @@ -25,27 +25,40 @@ public bool SingleReader(ref byte[] key, ref ObjectInput input, ref IGarnetObjec if (input.header.type != 0) { - if (input.header.type == GarnetObjectType.Ttl || input.header.type == GarnetObjectType.PTtl) // TTL command + switch (input.header.type) { - var ttlValue = input.header.type == GarnetObjectType.Ttl ? - ConvertUtils.SecondsFromDiffUtcNowTicks(value.Expiration > 0 ? value.Expiration : -1) : - ConvertUtils.MillisecondsFromDiffUtcNowTicks(value.Expiration > 0 ? value.Expiration : -1); - CopyRespNumber(ttlValue, ref dst.spanByteAndMemory); - return true; - } + case GarnetObjectType.Ttl: + var ttlValue = ConvertUtils.SecondsFromDiffUtcNowTicks(value.Expiration > 0 ? value.Expiration : -1); + CopyRespNumber(ttlValue, ref dst.spanByteAndMemory); + return true; + case GarnetObjectType.PTtl: + ttlValue = ConvertUtils.MillisecondsFromDiffUtcNowTicks(value.Expiration > 0 ? value.Expiration : -1); + CopyRespNumber(ttlValue, ref dst.spanByteAndMemory); + return true; + + case GarnetObjectType.Expiretime: + var expireTime = ConvertUtils.UnixTimeInSecondsFromTicks(value.Expiration > 0 ? value.Expiration : -1); + CopyRespNumber(expireTime, ref dst.spanByteAndMemory); + return true; + case GarnetObjectType.PExpiretime: + expireTime = ConvertUtils.UnixTimeInMillisecondsFromTicks(value.Expiration > 0 ? value.Expiration : -1); + CopyRespNumber(expireTime, ref dst.spanByteAndMemory); + return true; - if ((byte)input.header.type < CustomCommandManager.StartOffset) - return value.Operate(ref input, ref dst.spanByteAndMemory, out _, out _); + default: + if ((byte)input.header.type < CustomCommandManager.StartOffset) + return value.Operate(ref input, ref dst.spanByteAndMemory, out _, out _); - if (IncorrectObjectType(ref input, value, ref dst.spanByteAndMemory)) - return true; + if (IncorrectObjectType(ref input, value, ref dst.spanByteAndMemory)) + return true; - (IMemoryOwner Memory, int Length) outp = (dst.spanByteAndMemory.Memory, 0); - var customObjectCommand = GetCustomObjectCommand(ref input, input.header.type); - var result = customObjectCommand.Reader(key, ref input, value, ref outp, ref readInfo); - dst.spanByteAndMemory.Memory = outp.Memory; - dst.spanByteAndMemory.Length = outp.Length; - return result; + (IMemoryOwner Memory, int Length) outp = (dst.spanByteAndMemory.Memory, 0); + var customObjectCommand = GetCustomObjectCommand(ref input, input.header.type); + var result = customObjectCommand.Reader(key, ref input, value, ref outp, ref readInfo); + dst.spanByteAndMemory.Memory = outp.Memory; + dst.spanByteAndMemory.Length = outp.Length; + return result; + } } dst.garnetObject = value; diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 12637006e5..344c4a72b4 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -302,6 +302,71 @@ public unsafe GarnetStatus TTL(ref SpanByte key, Store return GarnetStatus.NOTFOUND; } + /// + /// Get the absolute Unix timestamp at which the given key will expire. + /// + /// + /// + /// The key to get the Unix timestamp. + /// The store to operate on + /// Span to allocate the output of the operation + /// Basic Context of the store + /// Object Context of the store + /// when true the command to execute is PEXPIRETIME. + /// Returns the absolute Unix timestamp (since January 1, 1970) in seconds or milliseconds at which the given key will expire. + public unsafe GarnetStatus EXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output, ref TContext context, ref TObjectContext objectContext, bool milliseconds = false) + where TContext : ITsavoriteContext + where TObjectContext : ITsavoriteContext + { + int inputSize = sizeof(int) + RespInputHeader.Size; + byte* pbCmdInput = stackalloc byte[inputSize]; + + byte* pcurr = pbCmdInput; + *(int*)pcurr = inputSize - sizeof(int); + pcurr += sizeof(int); + (*(RespInputHeader*)pcurr).cmd = milliseconds ? RespCommand.PEXPIRETIME : RespCommand.EXPIRETIME; + (*(RespInputHeader*)pcurr).flags = 0; + + if (storeType == StoreType.Main || storeType == StoreType.All) + { + var status = context.Read(ref key, ref Unsafe.AsRef(pbCmdInput), ref output); + + if (status.IsPending) + { + StartPendingMetrics(); + CompletePendingForSession(ref status, ref output, ref context); + StopPendingMetrics(); + } + + if (status.Found) return GarnetStatus.OK; + } + + if ((storeType == StoreType.Object || storeType == StoreType.All) && !objectStoreBasicContext.IsNull) + { + var objInput = new ObjectInput + { + header = new RespInputHeader + { + type = milliseconds ? GarnetObjectType.PExpiretime : GarnetObjectType.Expiretime, + }, + }; + + var keyBA = key.ToByteArray(); + var objO = new GarnetObjectStoreOutput { spanByteAndMemory = output }; + var status = objectContext.Read(ref keyBA, ref objInput, ref objO); + + if (status.IsPending) + CompletePendingForObjectStoreSession(ref status, ref objO, ref objectContext); + + if (status.Found) + { + output = objO.spanByteAndMemory; + return GarnetStatus.OK; + } + } + return GarnetStatus.NOTFOUND; + } + public GarnetStatus SET(ref SpanByte key, ref SpanByte value, ref TContext context) where TContext : ITsavoriteContext { diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 48176aabc5..6cd0d47eeb 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -112,6 +112,7 @@ public class SupportedCommand new("EXISTS", RespCommand.EXISTS), new("EXPIRE", RespCommand.EXPIRE), new("EXPIREAT", RespCommand.EXPIREAT), + new("EXPIRETIME", RespCommand.EXPIRETIME), new("FAILOVER", RespCommand.FAILOVER), new("FLUSHALL", RespCommand.FLUSHALL), new("FLUSHDB", RespCommand.FLUSHDB), @@ -184,6 +185,7 @@ public class SupportedCommand new("PERSIST", RespCommand.PERSIST), new("PEXPIRE", RespCommand.PEXPIRE), new("PEXPIREAT", RespCommand.PEXPIREAT), + new("PEXPIRETIME", RespCommand.PEXPIRETIME), new("PFADD", RespCommand.PFADD), new("PFCOUNT", RespCommand.PFCOUNT), new("PFMERGE", RespCommand.PFMERGE), diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 758ad68d30..0c7c5cfdb8 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -4411,6 +4411,36 @@ static async Task DoRegisterCSAsync(GarnetClient client) } } + [Test] + public async Task ExpireTimeACLsAsync() + { + await CheckCommandsAsync( + "EXPIRETIME", + [DoExpireTimeAsync] + ); + + static async Task DoExpireTimeAsync(GarnetClient client) + { + var val = await client.ExecuteForLongResultAsync("EXPIRETIME", ["foo"]); + ClassicAssert.AreEqual(-2, val); + } + } + + [Test] + public async Task PExpireTimeACLsAsync() + { + await CheckCommandsAsync( + "PEXPIRETIME", + [DoPExpireTimeAsync] + ); + + static async Task DoPExpireTimeAsync(GarnetClient client) + { + var val = await client.ExecuteForLongResultAsync("PEXPIRETIME", ["foo"]); + ClassicAssert.AreEqual(-2, val); + } + } + [Test] public async Task RenameACLsAsync() { diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 3c5aba514a..3e2e9c6243 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -1142,6 +1142,173 @@ public void MultipleExistsKeysAndObjects() ClassicAssert.AreEqual(3, exists); } + #region Expiretime + + [Test] + public void ExpiretimeWithStingValue() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + string key = "key1"; + var expireTimeSpan = TimeSpan.FromMinutes(1); + db.StringSet(key, "test1", expireTimeSpan); + + var actualExpireTime = (long)db.Execute("EXPIRETIME", key); + + ClassicAssert.GreaterOrEqual(actualExpireTime, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + var expireExpireTime = DateTimeOffset.UtcNow.Add(expireTimeSpan).ToUnixTimeSeconds(); + ClassicAssert.LessOrEqual(actualExpireTime, expireExpireTime); + } + + [Test] + public void ExpiretimeWithUnknownKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var expireTime = (long)db.Execute("EXPIRETIME", "keyZ"); + + ClassicAssert.AreEqual(-2, expireTime); + } + + [Test] + public void ExpiretimeWithNoKeyExpiration() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + string key = "key1"; + db.StringSet(key, "test1"); + + var expireTime = (long)db.Execute("EXPIRETIME", key); + + ClassicAssert.AreEqual(-1, expireTime); + } + + [Test] + public void ExpiretimeWithInvalidNumberOfArgs() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var exception = Assert.Throws(() => db.Execute("EXPIRETIME")); + Assert.That(exception.Message, Does.StartWith("ERR wrong number of arguments")); + } + + [Test] + public void ExpiretimeWithObjectValue() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + var expireTimeSpan = TimeSpan.FromMinutes(1); + var origList = new RedisValue[] { "a", "b", "c", "d" }; + var count = db.ListRightPush(key, origList); + var expirySet = db.KeyExpire(key, expireTimeSpan); + + var actualExpireTime = (long)db.Execute("EXPIRETIME", key); + + ClassicAssert.GreaterOrEqual(actualExpireTime, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + var expireExpireTime = DateTimeOffset.UtcNow.Add(expireTimeSpan).ToUnixTimeSeconds(); + ClassicAssert.LessOrEqual(actualExpireTime, expireExpireTime); + } + + [Test] + public void ExpiretimeWithNoKeyExpirationForObjectValue() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + var origList = new RedisValue[] { "a", "b", "c", "d" }; + var count = db.ListRightPush(key, origList); + + var expireTime = (long)db.Execute("EXPIRETIME", key); + + ClassicAssert.AreEqual(-1, expireTime); + } + + [Test] + public void PExpiretimeWithStingValue() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + string key = "key1"; + var expireTimeSpan = TimeSpan.FromMinutes(1); + db.StringSet(key, "test1", expireTimeSpan); + + var actualExpireTime = (long)db.Execute("PEXPIRETIME", key); + + ClassicAssert.GreaterOrEqual(actualExpireTime, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + var expireExpireTime = DateTimeOffset.UtcNow.Add(expireTimeSpan).ToUnixTimeMilliseconds(); + ClassicAssert.LessOrEqual(actualExpireTime, expireExpireTime); + } + + [Test] + public void PExpiretimeWithUnknownKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var expireTime = (long)db.Execute("PEXPIRETIME", "keyZ"); + + ClassicAssert.AreEqual(-2, expireTime); + } + + [Test] + public void PExpiretimeWithNoKeyExpiration() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + string key = "key1"; + db.StringSet(key, "test1"); + + var expireTime = (long)db.Execute("PEXPIRETIME", key); + + ClassicAssert.AreEqual(-1, expireTime); + } + + [Test] + public void PExpiretimeWithInvalidNumberOfArgs() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var exception = Assert.Throws(() => db.Execute("PEXPIRETIME")); + Assert.That(exception.Message, Does.StartWith("ERR wrong number of arguments")); + } + + [Test] + public void PExpiretimeWithObjectValue() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + var expireTimeSpan = TimeSpan.FromMinutes(1); + var origList = new RedisValue[] { "a", "b", "c", "d" }; + var count = db.ListRightPush(key, origList); + var expirySet = db.KeyExpire(key, expireTimeSpan); + + var actualExpireTime = (long)db.Execute("PEXPIRETIME", key); + + ClassicAssert.GreaterOrEqual(actualExpireTime, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + var expireExpireTime = DateTimeOffset.UtcNow.Add(expireTimeSpan).ToUnixTimeMilliseconds(); + ClassicAssert.LessOrEqual(actualExpireTime, expireExpireTime); + } + + [Test] + public void PExpiretimeWithNoKeyExpirationForObjectValue() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + var origList = new RedisValue[] { "a", "b", "c", "d" }; + var count = db.ListRightPush(key, origList); + + var expireTime = (long)db.Execute("PEXPIRETIME", key); + + ClassicAssert.AreEqual(-1, expireTime); + } + + #endregion [Test] public void SingleRename() diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index febf726cba..dfecea3a36 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -136,7 +136,7 @@ Note that this list is subject to change as we continue to expand our API comman | **GENERIC** | [PERSIST](generic-commands.md#persist) | ➕ | | | | [PEXPIRE](generic-commands.md#pexpire) | ➕ | | | | [PEXPIREAT](generic-commands.md#pexpireat) | ➕ | | -| | PEXPIRETIME | ➖ | | +| | [PEXPIRETIME](generic-commands.md#pexpiretime) | ➕ | | | | [PTTL](generic-commands.md#pttl) | ➕ | | | | RANDOMKEY | ➖ | | | | [RENAME](generic-commands.md#rename) | ➕ | | @@ -197,7 +197,7 @@ Note that this list is subject to change as we continue to expand our API comman | | [EXISTS](generic-commands.md#exists) | ➕ | | | | [EXPIRE](generic-commands.md#expire) | ➕ | | | | [EXPIREAT](generic-commands.md#expireat) | ➕ | | -| | EXPIRETIME | ➖ | | +| | [EXPIRETIME](generic-commands.md#expiretime) | ➕ | | | | [KEYS](generic-commands.md#keys) | ➕ | | | | [MIGRATE](generic-commands.md#migrate) | ➕ | | | | MOVE | ➖ | | diff --git a/website/docs/commands/generic-commands.md b/website/docs/commands/generic-commands.md index 850130f84b..59ebf9b933 100644 --- a/website/docs/commands/generic-commands.md +++ b/website/docs/commands/generic-commands.md @@ -205,6 +205,26 @@ One of the following: --- +### EXPIRETIME + +#### Syntax + +```bash + EXPIRETIME key +``` + +Returns the absolute Unix timestamp (since January 1, 1970) in seconds at which the given key will expire. + +#### Resp Reply + +One of the following: + +* Integer reply: Expiration Unix timestamp in milliseconds. +* Integer reply: -1 if the key exists but has no associated expiration time. +* Integer reply: -2 if the key does not exist. + +--- + ### KEYS #### Syntax @@ -273,6 +293,26 @@ One of the following: --- +### PEXPIRETIME + +#### Syntax + +```bash + PEXPIRETIME key +``` + +Returns the absolute Unix timestamp (since January 1, 1970) in milliseconds at which the given key will expire. + +#### Resp Reply + +One of the following: + +* Integer reply: Expiration Unix timestamp in milliseconds. +* Integer reply: -1 if the key exists but has no associated expiration time. +* Integer reply: -2 if the key does not exist. + +--- + ### PEXPIREAT #### Syntax