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