Skip to content

Commit 748ec9a

Browse files
authored
[Compatibility] Added EXPIREAT and PEXPIREAT command and bug fixes for 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
1 parent 64636ce commit 748ec9a

File tree

16 files changed

+1070
-18
lines changed

16 files changed

+1070
-18
lines changed

libs/common/ConvertUtils.cs

+28-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
using System;
5-
using System.Diagnostics;
5+
using System.Runtime.CompilerServices;
66

77
namespace Garnet.common
88
{
@@ -11,6 +11,11 @@ namespace Garnet.common
1111
/// </summary>
1212
public static class ConvertUtils
1313
{
14+
/// <summary>
15+
/// Contains the number of ticks representing 1970/1/1. Value is equal to new DateTime(1970, 1, 1).Ticks
16+
/// </summary>
17+
private static readonly long _unixEpochTicks = DateTimeOffset.UnixEpoch.Ticks;
18+
1419
/// <summary>
1520
/// Convert diff ticks - utcNow.ticks to seconds.
1621
/// </summary>
@@ -43,5 +48,27 @@ public static long MillisecondsFromDiffUtcNowTicks(long ticks)
4348
}
4449
return milliseconds;
4550
}
51+
52+
/// <summary>
53+
/// Converts a Unix timestamp in seconds to ticks.
54+
/// </summary>
55+
/// <param name="unixTimestamp">The Unix timestamp in seconds.</param>
56+
/// <returns>The equivalent number of ticks.</returns>
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
58+
public static long UnixTimestampInSecondsToTicks(long unixTimestamp)
59+
{
60+
return unixTimestamp * TimeSpan.TicksPerSecond + _unixEpochTicks;
61+
}
62+
63+
/// <summary>
64+
/// Converts a Unix timestamp in milliseconds to ticks.
65+
/// </summary>
66+
/// <param name="unixTimestamp">The Unix timestamp in milliseconds.</param>
67+
/// <returns>The equivalent number of ticks.</returns>
68+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
69+
public static long UnixTimestampInMillisecondsToTicks(long unixTimestamp)
70+
{
71+
return unixTimestamp * TimeSpan.TicksPerMillisecond + _unixEpochTicks;
72+
}
4673
}
4774
}

libs/server/API/GarnetApi.cs

+12
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ public GarnetStatus PEXPIRE(ArgSlice key, TimeSpan expiry, out bool timeoutSet,
181181

182182
#endregion
183183

184+
#region EXPIREAT
185+
186+
/// <inheritdoc />
187+
public GarnetStatus EXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None)
188+
=> storageSession.EXPIREAT(key, expiryTimestamp, out timeoutSet, storeType, expireOption, ref context, ref objectContext);
189+
190+
/// <inheritdoc />
191+
public GarnetStatus PEXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None)
192+
=> storageSession.EXPIREAT(key, expiryTimestamp, out timeoutSet, storeType, expireOption, ref context, ref objectContext, milliseconds: true);
193+
194+
#endregion
195+
184196
#region PERSIST
185197
/// <inheritdoc />
186198
public unsafe GarnetStatus PERSIST(ArgSlice key, StoreType storeType = StoreType.All)

libs/server/API/IGarnetApi.cs

+26
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,32 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
176176

177177
#endregion
178178

179+
#region EXPIREAT
180+
181+
/// <summary>
182+
/// Set a timeout on key using absolute Unix timestamp (seconds since January 1, 1970) in seconds
183+
/// </summary>
184+
/// <param name="key">Key</param>
185+
/// <param name="expiryTimestamp">Absolute Unix timestamp in seconds</param>
186+
/// <param name="timeoutSet">Whether timeout was set by the call</param>
187+
/// <param name="storeType">Store type: main, object, or both</param>
188+
/// <param name="expireOption">Expire option</param>
189+
/// <returns></returns>
190+
GarnetStatus EXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None);
191+
192+
/// <summary>
193+
/// Set a timeout on key using absolute Unix timestamp (seconds since January 1, 1970) in milliseconds
194+
/// </summary>
195+
/// <param name="key">Key</param>
196+
/// <param name="expiryTimestamp">Absolute Unix timestamp in milliseconds</param>
197+
/// <param name="timeoutSet">Whether timeout was set by the call</param>
198+
/// <param name="storeType">Store type: main, object, or both</param>
199+
/// <param name="expireOption">Expire option</param>
200+
/// <returns></returns>
201+
GarnetStatus PEXPIREAT(ArgSlice key, long expiryTimestamp, out bool timeoutSet, StoreType storeType = StoreType.All, ExpireOption expireOption = ExpireOption.None);
202+
203+
#endregion
204+
179205
#region PERSIST
180206
/// <summary>
181207
/// PERSIST

libs/server/ExpireOption.cs

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,43 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4+
using System;
5+
46
namespace Garnet.server
57
{
68
/// <summary>
79
/// Expire option
810
/// </summary>
11+
[Flags]
912
public enum ExpireOption : byte
1013
{
1114
/// <summary>
1215
/// None
1316
/// </summary>
14-
None,
17+
None = 0,
1518
/// <summary>
1619
/// Set expiry only when the key has no expiry
1720
/// </summary>
18-
NX,
21+
NX = 1 << 0,
1922
/// <summary>
2023
/// Set expiry only when the key has an existing expiry
2124
/// </summary>
22-
XX,
25+
XX = 1 << 1,
2326
/// <summary>
2427
/// Set expiry only when the new expiry is greater than current one
2528
/// </summary>
26-
GT,
29+
GT = 1 << 2,
2730
/// <summary>
2831
/// Set expiry only when the new expiry is less than current one
2932
/// </summary>
30-
LT
33+
LT = 1 << 3,
34+
/// <summary>
35+
/// Set expiry only when the key has an existing expiry and the new expiry is greater than current one
36+
/// </summary>
37+
XXGT = XX | GT,
38+
/// <summary>
39+
/// Set expiry only when the key has an existing expiry and the new expiry is less than current one
40+
/// </summary>
41+
XXLT = XX | LT,
3142
}
3243
}

libs/server/Resp/KeyAdminCommands.cs

+117-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ private bool NetworkEXPIRE<TGarnetApi>(RespCommand command, ref TGarnetApi stora
174174
where TGarnetApi : IGarnetApi
175175
{
176176
var count = parseState.Count;
177-
if (count < 2 || count > 3)
177+
if (count < 2 || count > 4)
178178
{
179179
return AbortWithWrongNumberOfArguments(nameof(RespCommand.EXPIRE));
180180
}
@@ -205,6 +205,36 @@ private bool NetworkEXPIRE<TGarnetApi>(RespCommand command, ref TGarnetApi stora
205205
}
206206
}
207207

208+
if (parseState.Count > 3)
209+
{
210+
if (!TryGetExpireOption(parseState.GetArgSliceByRef(3).ReadOnlySpan, out var additionExpireOption))
211+
{
212+
var optionStr = parseState.GetString(3);
213+
214+
while (!RespWriteUtils.WriteError($"ERR Unsupported option {optionStr}", ref dcurr, dend))
215+
SendAndReset();
216+
return true;
217+
}
218+
219+
if (expireOption == ExpireOption.XX && (additionExpireOption == ExpireOption.GT || additionExpireOption == ExpireOption.LT))
220+
{
221+
expireOption = ExpireOption.XX | additionExpireOption;
222+
}
223+
else if (expireOption == ExpireOption.GT && additionExpireOption == ExpireOption.XX)
224+
{
225+
expireOption = ExpireOption.XXGT;
226+
}
227+
else if (expireOption == ExpireOption.LT && additionExpireOption == ExpireOption.XX)
228+
{
229+
expireOption = ExpireOption.XXLT;
230+
}
231+
else
232+
{
233+
while (!RespWriteUtils.WriteError("ERR NX and XX, GT or LT options at the same time are not compatible", ref dcurr, dend))
234+
SendAndReset();
235+
}
236+
}
237+
208238
var status = command == RespCommand.EXPIRE ?
209239
storageApi.EXPIRE(key, expiryMs, out var timeoutSet, StoreType.All, expireOption) :
210240
storageApi.PEXPIRE(key, expiryMs, out timeoutSet, StoreType.All, expireOption);
@@ -223,6 +253,92 @@ private bool NetworkEXPIRE<TGarnetApi>(RespCommand command, ref TGarnetApi stora
223253
return true;
224254
}
225255

256+
/// <summary>
257+
/// Set a timeout on a key based on unix timestamp
258+
/// </summary>
259+
/// <typeparam name="TGarnetApi"></typeparam>
260+
/// <param name="command">Indicates which command to use, expire or pexpire.</param>
261+
/// <param name="storageApi"></param>
262+
/// <returns></returns>
263+
private bool NetworkEXPIREAT<TGarnetApi>(RespCommand command, ref TGarnetApi storageApi)
264+
where TGarnetApi : IGarnetApi
265+
{
266+
var count = parseState.Count;
267+
if (count < 2 || count > 4)
268+
{
269+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.EXPIREAT));
270+
}
271+
272+
var key = parseState.GetArgSliceByRef(0);
273+
if (!parseState.TryGetLong(1, out var expiryTimestamp))
274+
{
275+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend))
276+
SendAndReset();
277+
return true;
278+
}
279+
280+
var expireOption = ExpireOption.None;
281+
282+
if (parseState.Count > 2)
283+
{
284+
if (!TryGetExpireOption(parseState.GetArgSliceByRef(2).ReadOnlySpan, out expireOption))
285+
{
286+
var optionStr = parseState.GetString(2);
287+
288+
while (!RespWriteUtils.WriteError($"ERR Unsupported option {optionStr}", ref dcurr, dend))
289+
SendAndReset();
290+
return true;
291+
}
292+
}
293+
294+
if (parseState.Count > 3)
295+
{
296+
if (!TryGetExpireOption(parseState.GetArgSliceByRef(3).ReadOnlySpan, out var additionExpireOption))
297+
{
298+
var optionStr = parseState.GetString(3);
299+
300+
while (!RespWriteUtils.WriteError($"ERR Unsupported option {optionStr}", ref dcurr, dend))
301+
SendAndReset();
302+
return true;
303+
}
304+
305+
if (expireOption == ExpireOption.XX && (additionExpireOption == ExpireOption.GT || additionExpireOption == ExpireOption.LT))
306+
{
307+
expireOption = ExpireOption.XX | additionExpireOption;
308+
}
309+
else if (expireOption == ExpireOption.GT && additionExpireOption == ExpireOption.XX)
310+
{
311+
expireOption = ExpireOption.XXGT;
312+
}
313+
else if (expireOption == ExpireOption.LT && additionExpireOption == ExpireOption.XX)
314+
{
315+
expireOption = ExpireOption.XXLT;
316+
}
317+
else
318+
{
319+
while (!RespWriteUtils.WriteError("ERR NX and XX, GT or LT options at the same time are not compatible", ref dcurr, dend))
320+
SendAndReset();
321+
}
322+
}
323+
324+
var status = command == RespCommand.EXPIREAT ?
325+
storageApi.EXPIREAT(key, expiryTimestamp, out var timeoutSet, StoreType.All, expireOption) :
326+
storageApi.PEXPIREAT(key, expiryTimestamp, out timeoutSet, StoreType.All, expireOption);
327+
328+
if (status == GarnetStatus.OK && timeoutSet)
329+
{
330+
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_1, ref dcurr, dend))
331+
SendAndReset();
332+
}
333+
else
334+
{
335+
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend))
336+
SendAndReset();
337+
}
338+
339+
return true;
340+
}
341+
226342
/// <summary>
227343
/// PERSIST command
228344
/// </summary>

libs/server/Resp/Parser/RespCommand.cs

+10
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public enum RespCommand : byte
8585
DECRBY,
8686
DEL,
8787
EXPIRE,
88+
EXPIREAT,
8889
FLUSHALL,
8990
FLUSHDB,
9091
GEOADD,
@@ -114,6 +115,7 @@ public enum RespCommand : byte
114115
MSETNX,
115116
PERSIST,
116117
PEXPIRE,
118+
PEXPIREAT,
117119
PFADD,
118120
PFMERGE,
119121
PSETEX,
@@ -1252,6 +1254,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
12521254
{
12531255
return RespCommand.BITFIELD;
12541256
}
1257+
else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("EXPIREAT"u8) && *(ushort*)(ptr + 12) == MemoryMarshal.Read<ushort>("\r\n"u8))
1258+
{
1259+
return RespCommand.EXPIREAT;
1260+
}
12551261
break;
12561262
case 9:
12571263
if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("SUBSCRIB"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read<uint>("BE\r\n"u8))
@@ -1278,6 +1284,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
12781284
{
12791285
return RespCommand.RPOPLPUSH;
12801286
}
1287+
else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read<ulong>("PEXPIREA"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read<uint>("AT\r\n"u8))
1288+
{
1289+
return RespCommand.PEXPIREAT;
1290+
}
12811291
break;
12821292
}
12831293

0 commit comments

Comments
 (0)