Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Compatibility] Adding EXPIRETIME and PEXPIRETIME command #664

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions libs/common/ConvertUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace Garnet.common
/// </summary>
public static class ConvertUtils
{
/// <summary>
/// Contains the number of ticks representing 1970/1/1. Value is equal to new DateTime(1970, 1, 1).Ticks
/// </summary>
private const long _unixTillStartTimeTicks = 621355968000000000;

/// <summary>
/// Convert diff ticks - utcNow.ticks to seconds.
/// </summary>
Expand Down Expand Up @@ -43,5 +48,25 @@ public static long MillisecondsFromDiffUtcNowTicks(long ticks)
}
return milliseconds;
}

/// <summary>
/// Convert ticks to Unix time in seconds.
/// </summary>
/// <param name="ticks">The ticks to convert.</param>
/// <returns>The Unix time in seconds.</returns>
public static long UnixTimeInSecondsFromTicks(long ticks)
{
return ticks > 0 ? (ticks - _unixTillStartTimeTicks) / TimeSpan.TicksPerSecond : -1;
}

/// <summary>
/// Convert ticks to Unix time in milliseconds.
/// </summary>
/// <param name="ticks">The ticks to convert.</param>
/// <returns>The Unix time in milliseconds.</returns>
public static long UnixTimeInMillisecondsFromTicks(long ticks)
{
return ticks > 0 ? (ticks - _unixTillStartTimeTicks) / TimeSpan.TicksPerMillisecond : -1;
}
}
}
12 changes: 12 additions & 0 deletions libs/server/API/GarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ public GarnetStatus PTTL(ref SpanByte key, StoreType storeType, ref SpanByteAndM

#endregion

#region EXPIRETIME

/// <inheritdoc />
public GarnetStatus EXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output)
=> storageSession.EXPIRETIME(ref key, storeType, ref output, ref context, ref objectContext);

/// <inheritdoc />
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
/// <inheritdoc />
public GarnetStatus SET(ref SpanByte key, ref SpanByte value)
Expand Down
18 changes: 18 additions & 0 deletions libs/server/API/GarnetWatchApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ public GarnetStatus PTTL(ref SpanByte key, StoreType storeType, ref SpanByteAndM

#endregion

#region EXPIRETIME

/// <inheritdoc />
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);
}

/// <inheritdoc />
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

/// <inheritdoc />
Expand Down
22 changes: 22 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,28 @@ public interface IGarnetReadApi

#endregion

#region EXPIRETIME

/// <summary>
/// Returns the absolute Unix timestamp (since January 1, 1970) in seconds at which the given key will expire.
/// </summary>
/// <param name="key">The key to get the expiration time for.</param>
/// <param name="storeType">The type of store to retrieve the key from.</param>
/// <param name="output">The output containing the expiration time.</param>
/// <returns>The status of the operation.</returns>
GarnetStatus EXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output);

/// <summary>
/// Returns the absolute Unix timestamp (since January 1, 1970) in milliseconds at which the given key will expire.
/// </summary>
/// <param name="key">The key to get the expiration time for.</param>
/// <param name="storeType">The type of store to retrieve the key from.</param>
/// <param name="output">The output containing the expiration time.</param>
/// <returns>The status of the operation.</returns>
GarnetStatus PEXPIRETIME(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output);

#endregion

#region SortedSet Methods

/// <summary>
Expand Down
10 changes: 10 additions & 0 deletions libs/server/Objects/Types/GarnetObjectType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ public enum GarnetObjectType : byte
/// </summary>
Set,

/// <summary>
/// Special type indicating EXPIRETIME command
/// </summary>
Expiretime = 0xf9,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think that there's a need for these new types


/// <summary>
/// Special type indicating PEXPIRETIME command
/// </summary>
PExpiretime = 0xfa,

/// <summary>
/// Special type indicating PERSIST command
/// </summary>
Expand Down
36 changes: 36 additions & 0 deletions libs/server/Resp/KeyAdminCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,41 @@ private bool NetworkTTL<TGarnetApi>(RespCommand command, ref TGarnetApi storageA
}
return true;
}

/// <summary>
/// Get the absolute Unix timestamp at which the given key will expire.
/// </summary>
/// <typeparam name="TGarnetApi"></typeparam>
/// <param name="command">either if the call is for EXPIRETIME or PEXPIRETIME command</param>
/// <param name="storageApi"></param>
/// <returns>Returns the absolute Unix timestamp (since January 1, 1970) in seconds or milliseconds at which the given key will expire.</returns>
private bool NetworkEXPIRETIME<TGarnetApi>(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;
}
}
}
10 changes: 10 additions & 0 deletions libs/server/Resp/Parser/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum RespCommand : byte
COSCAN,
DBSIZE,
EXISTS,
EXPIRETIME,
GEODIST,
GEOHASH,
GEOPOS,
Expand All @@ -48,6 +49,7 @@ public enum RespCommand : byte
LRANGE,
MEMORY_USAGE,
MGET,
PEXPIRETIME,
PFCOUNT,
PTTL,
SCAN,
Expand Down Expand Up @@ -1311,6 +1313,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.SDIFFSTORE;
}
else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read<ulong>("10\r\nEXPI"u8) && *(uint*)(ptr + 9) == MemoryMarshal.Read<uint>("RETIME\r\n"u8))
{
return RespCommand.EXPIRETIME;
}
break;

case 11:
Expand Down Expand Up @@ -1338,6 +1344,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.SINTERSTORE;
}
else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read<ulong>("1\r\nPEXPI"u8) && *(uint*)(ptr + 10) == MemoryMarshal.Read<uint>("RETIME\r\n"u8))
{
return RespCommand.PEXPIRETIME;
}
break;

case 12:
Expand Down
58 changes: 58 additions & 0 deletions libs/server/Resp/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,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",
Expand Down Expand Up @@ -3254,6 +3283,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",
Expand Down
2 changes: 2 additions & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ private bool ProcessBasicCommands<TGarnetApi>(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),
Expand Down
11 changes: 11 additions & 0 deletions libs/server/Storage/Functions/MainStore/PrivateMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
9 changes: 9 additions & 0 deletions libs/server/Storage/Functions/ObjectStore/ReadMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ public bool SingleReader(ref byte[] key, ref ObjectInput input, ref IGarnetObjec
if ((byte)input.header.type < CustomCommandManager.StartOffset)
return value.Operate(ref input, ref dst.spanByteAndMemory, out _, out _);

if (input.header.type == GarnetObjectType.Expiretime || input.header.type == GarnetObjectType.PExpiretime)
{
var expireTime = input.header.type == GarnetObjectType.Expiretime ?
ConvertUtils.UnixTimeInSecondsFromTicks(value.Expiration > 0 ? value.Expiration : -1) :
ConvertUtils.UnixTimeInMillisecondsFromTicks(value.Expiration > 0 ? value.Expiration : -1);
CopyRespNumber(expireTime, ref dst.spanByteAndMemory);
return true;
}

if (IncorrectObjectType(ref input, value, ref dst.spanByteAndMemory))
return true;

Expand Down
66 changes: 66 additions & 0 deletions libs/server/Storage/Session/MainStore/MainStoreOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,72 @@ public unsafe GarnetStatus TTL<TContext, TObjectContext>(ref SpanByte key, Store
return GarnetStatus.NOTFOUND;
}

/// <summary>
/// Get the absolute Unix timestamp at which the given key will expire.
/// </summary>
/// <typeparam name="TContext"></typeparam>
/// <typeparam name="TObjectContext"></typeparam>
/// <param name="key">The key to get the Unix timestamp.</param>
/// <param name="storeType">The store to operate on</param>
/// <param name="output">Span to allocate the output of the operation</param>
/// <param name="context">Basic Context of the store</param>
/// <param name="objectContext">Object Context of the store</param>
/// <param name="milliseconds">when true the command to execute is PEXPIRETIME.</param>
/// <returns>Returns the absolute Unix timestamp (since January 1, 1970) in seconds or milliseconds at which the given key will expire.</returns>
public unsafe GarnetStatus EXPIRETIME<TContext, TObjectContext>(ref SpanByte key, StoreType storeType, ref SpanByteAndMemory output, ref TContext context, ref TObjectContext objectContext, bool milliseconds = false)
where TContext : ITsavoriteContext<SpanByte, SpanByte, SpanByte, SpanByteAndMemory, long, MainSessionFunctions, MainStoreFunctions, MainStoreAllocator>
where TObjectContext : ITsavoriteContext<byte[], IGarnetObject, ObjectInput, GarnetObjectStoreOutput, long, ObjectSessionFunctions, ObjectStoreFunctions, ObjectStoreAllocator>
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply call TTL and convert it to unix-time?

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<SpanByte>(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
{
cmd = milliseconds ? RespCommand.PEXPIRETIME : RespCommand.EXPIRETIME,
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<TContext>(ref SpanByte key, ref SpanByte value, ref TContext context)
where TContext : ITsavoriteContext<SpanByte, SpanByte, SpanByte, SpanByteAndMemory, long, MainSessionFunctions, MainStoreFunctions, MainStoreAllocator>
{
Expand Down
Loading