Skip to content

Commit

Permalink
[Compatibility] Added EXPIREAT and PEXPIREAT command and bug fixes fo…
Browse files Browse the repository at this point in the history
…r EXPIRE and PEXPIRE (#666)

* Added EXPIREAT and PEXPIREAT command and bug fixes for EXPIRE and PEXPIRE

* Code style fix

* Review comment fixes

* Fixed merge conflict and fixed review comments

* Changed to _unixEpochTicks
  • Loading branch information
Vijay-Nirmal authored Oct 4, 2024
1 parent 64636ce commit 748ec9a
Show file tree
Hide file tree
Showing 16 changed files with 1,070 additions and 18 deletions.
29 changes: 28 additions & 1 deletion libs/common/ConvertUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Garnet.common
{
Expand All @@ -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 static readonly long _unixEpochTicks = DateTimeOffset.UnixEpoch.Ticks;

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

/// <summary>
/// Converts a Unix timestamp in seconds to ticks.
/// </summary>
/// <param name="unixTimestamp">The Unix timestamp in seconds.</param>
/// <returns>The equivalent number of ticks.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long UnixTimestampInSecondsToTicks(long unixTimestamp)
{
return unixTimestamp * TimeSpan.TicksPerSecond + _unixEpochTicks;
}

/// <summary>
/// Converts a Unix timestamp in milliseconds to ticks.
/// </summary>
/// <param name="unixTimestamp">The Unix timestamp in milliseconds.</param>
/// <returns>The equivalent number of ticks.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long UnixTimestampInMillisecondsToTicks(long unixTimestamp)
{
return unixTimestamp * TimeSpan.TicksPerMillisecond + _unixEpochTicks;
}
}
}
12 changes: 12 additions & 0 deletions libs/server/API/GarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ public GarnetStatus PEXPIRE(ArgSlice key, TimeSpan expiry, out bool timeoutSet,

#endregion

#region EXPIREAT

/// <inheritdoc />
public GarnetStatus EXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None)
=> storageSession.EXPIREAT(key, expiryTimestamp, out timeoutSet, storeType, expireOption, ref context, ref objectContext);

/// <inheritdoc />
public GarnetStatus PEXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None)
=> storageSession.EXPIREAT(key, expiryTimestamp, out timeoutSet, storeType, expireOption, ref context, ref objectContext, milliseconds: true);

#endregion

#region PERSIST
/// <inheritdoc />
public unsafe GarnetStatus PERSIST(ArgSlice key, StoreType storeType = StoreType.All)
Expand Down
26 changes: 26 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,32 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi

#endregion

#region EXPIREAT

/// <summary>
/// Set a timeout on key using absolute Unix timestamp (seconds since January 1, 1970) in seconds
/// </summary>
/// <param name="key">Key</param>
/// <param name="expiryTimestamp">Absolute Unix timestamp in seconds</param>
/// <param name="timeoutSet">Whether timeout was set by the call</param>
/// <param name="storeType">Store type: main, object, or both</param>
/// <param name="expireOption">Expire option</param>
/// <returns></returns>
GarnetStatus EXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None);

/// <summary>
/// Set a timeout on key using absolute Unix timestamp (seconds since January 1, 1970) in milliseconds
/// </summary>
/// <param name="key">Key</param>
/// <param name="expiryTimestamp">Absolute Unix timestamp in milliseconds</param>
/// <param name="timeoutSet">Whether timeout was set by the call</param>
/// <param name="storeType">Store type: main, object, or both</param>
/// <param name="expireOption">Expire option</param>
/// <returns></returns>
GarnetStatus PEXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None);

#endregion

#region PERSIST
/// <summary>
/// PERSIST
Expand Down
21 changes: 16 additions & 5 deletions libs/server/ExpireOption.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;

namespace Garnet.server
{
/// <summary>
/// Expire option
/// </summary>
[Flags]
public enum ExpireOption : byte
{
/// <summary>
/// None
/// </summary>
None,
None = 0,
/// <summary>
/// Set expiry only when the key has no expiry
/// </summary>
NX,
NX = 1 << 0,
/// <summary>
/// Set expiry only when the key has an existing expiry
/// </summary>
XX,
XX = 1 << 1,
/// <summary>
/// Set expiry only when the new expiry is greater than current one
/// </summary>
GT,
GT = 1 << 2,
/// <summary>
/// Set expiry only when the new expiry is less than current one
/// </summary>
LT
LT = 1 << 3,
/// <summary>
/// Set expiry only when the key has an existing expiry and the new expiry is greater than current one
/// </summary>
XXGT = XX | GT,
/// <summary>
/// Set expiry only when the key has an existing expiry and the new expiry is less than current one
/// </summary>
XXLT = XX | LT,
}
}
118 changes: 117 additions & 1 deletion libs/server/Resp/KeyAdminCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ private bool NetworkEXPIRE<TGarnetApi>(RespCommand command, ref TGarnetApi stora
where TGarnetApi : IGarnetApi
{
var count = parseState.Count;
if (count < 2 || count > 3)
if (count < 2 || count > 4)
{
return AbortWithWrongNumberOfArguments(nameof(RespCommand.EXPIRE));
}
Expand Down Expand Up @@ -205,6 +205,36 @@ private bool NetworkEXPIRE<TGarnetApi>(RespCommand command, ref TGarnetApi stora
}
}

if (parseState.Count > 3)
{
if (!TryGetExpireOption(parseState.GetArgSliceByRef(3).ReadOnlySpan, out var additionExpireOption))
{
var optionStr = parseState.GetString(3);

while (!RespWriteUtils.WriteError($"ERR Unsupported option {optionStr}", ref dcurr, dend))
SendAndReset();
return true;
}

if (expireOption == ExpireOption.XX && (additionExpireOption == ExpireOption.GT || additionExpireOption == ExpireOption.LT))
{
expireOption = ExpireOption.XX | additionExpireOption;
}
else if (expireOption == ExpireOption.GT && additionExpireOption == ExpireOption.XX)
{
expireOption = ExpireOption.XXGT;
}
else if (expireOption == ExpireOption.LT && additionExpireOption == ExpireOption.XX)
{
expireOption = ExpireOption.XXLT;
}
else
{
while (!RespWriteUtils.WriteError("ERR NX and XX, GT or LT options at the same time are not compatible", ref dcurr, dend))
SendAndReset();
}
}

var status = command == RespCommand.EXPIRE ?
storageApi.EXPIRE(key, expiryMs, out var timeoutSet, StoreType.All, expireOption) :
storageApi.PEXPIRE(key, expiryMs, out timeoutSet, StoreType.All, expireOption);
Expand All @@ -223,6 +253,92 @@ private bool NetworkEXPIRE<TGarnetApi>(RespCommand command, ref TGarnetApi stora
return true;
}

/// <summary>
/// Set a timeout on a key based on unix timestamp
/// </summary>
/// <typeparam name="TGarnetApi"></typeparam>
/// <param name="command">Indicates which command to use, expire or pexpire.</param>
/// <param name="storageApi"></param>
/// <returns></returns>
private bool NetworkEXPIREAT<TGarnetApi>(RespCommand command, ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
var count = parseState.Count;
if (count < 2 || count > 4)
{
return AbortWithWrongNumberOfArguments(nameof(RespCommand.EXPIREAT));
}

var key = parseState.GetArgSliceByRef(0);
if (!parseState.TryGetLong(1, out var expiryTimestamp))
{
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend))
SendAndReset();
return true;
}

var expireOption = ExpireOption.None;

if (parseState.Count > 2)
{
if (!TryGetExpireOption(parseState.GetArgSliceByRef(2).ReadOnlySpan, out expireOption))
{
var optionStr = parseState.GetString(2);

while (!RespWriteUtils.WriteError($"ERR Unsupported option {optionStr}", ref dcurr, dend))
SendAndReset();
return true;
}
}

if (parseState.Count > 3)
{
if (!TryGetExpireOption(parseState.GetArgSliceByRef(3).ReadOnlySpan, out var additionExpireOption))
{
var optionStr = parseState.GetString(3);

while (!RespWriteUtils.WriteError($"ERR Unsupported option {optionStr}", ref dcurr, dend))
SendAndReset();
return true;
}

if (expireOption == ExpireOption.XX && (additionExpireOption == ExpireOption.GT || additionExpireOption == ExpireOption.LT))
{
expireOption = ExpireOption.XX | additionExpireOption;
}
else if (expireOption == ExpireOption.GT && additionExpireOption == ExpireOption.XX)
{
expireOption = ExpireOption.XXGT;
}
else if (expireOption == ExpireOption.LT && additionExpireOption == ExpireOption.XX)
{
expireOption = ExpireOption.XXLT;
}
else
{
while (!RespWriteUtils.WriteError("ERR NX and XX, GT or LT options at the same time are not compatible", ref dcurr, dend))
SendAndReset();
}
}

var status = command == RespCommand.EXPIREAT ?
storageApi.EXPIREAT(key, expiryTimestamp, out var timeoutSet, StoreType.All, expireOption) :
storageApi.PEXPIREAT(key, expiryTimestamp, out timeoutSet, StoreType.All, expireOption);

if (status == GarnetStatus.OK && timeoutSet)
{
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_1, ref dcurr, dend))
SendAndReset();
}
else
{
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend))
SendAndReset();
}

return true;
}

/// <summary>
/// PERSIST command
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions libs/server/Resp/Parser/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public enum RespCommand : byte
DECRBY,
DEL,
EXPIRE,
EXPIREAT,
FLUSHALL,
FLUSHDB,
GEOADD,
Expand Down Expand Up @@ -114,6 +115,7 @@ public enum RespCommand : byte
MSETNX,
PERSIST,
PEXPIRE,
PEXPIREAT,
PFADD,
PFMERGE,
PSETEX,
Expand Down Expand Up @@ -1252,6 +1254,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.BITFIELD;
}
else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("EXPIREAT"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read<ushort>("\r\n"u8))
{
return RespCommand.EXPIREAT;
}
break;
case 9:
if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("SUBSCRIB"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read<uint>("BE\r\n"u8))
Expand All @@ -1278,6 +1284,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.RPOPLPUSH;
}
else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("PEXPIREA"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read<uint>("AT\r\n"u8))
{
return RespCommand.PEXPIREAT;
}
break;
}

Expand Down
Loading

0 comments on commit 748ec9a

Please sign in to comment.