Skip to content

Commit 89626ce

Browse files
Add DELIFGREATER ETag Command (#1179)
* Add new etag command DELIFGREATER * Make DELIFGREATER work * Add Command Test for ACL * Add documentation * Updates * fmt * Fix txn manager * Add RCU NCU in unit test and fix * fix * FMT * Fix GETDEL overallocation * easy money --------- Co-authored-by: Hamdaan Khalid <[email protected]>
1 parent d609b01 commit 89626ce

File tree

19 files changed

+453
-19
lines changed

19 files changed

+453
-19
lines changed

libs/resources/RespCommandsDocs.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,29 @@
18951895
}
18961896
]
18971897
},
1898+
{
1899+
"Command": "DELIFGREATER",
1900+
"Name": "DELIFGREATER",
1901+
"Summary": "Deletes a key only if the provided Etag is strictly greater than the existing Etag for the key.",
1902+
"Group": "Generic",
1903+
"Complexity": "O(1)",
1904+
"Arguments": [
1905+
{
1906+
"TypeDiscriminator": "RespCommandKeyArgument",
1907+
"Name": "KEY",
1908+
"DisplayText": "key",
1909+
"Type": "Key",
1910+
"KeySpecIndex": 0
1911+
},
1912+
{
1913+
"TypeDiscriminator": "RespCommandBasicArgument",
1914+
"Name": "ETAG",
1915+
"DisplayText": "etag",
1916+
"Type": "Integer"
1917+
}
1918+
]
1919+
},
1920+
18981921
{
18991922
"Command": "DISCARD",
19001923
"Name": "DISCARD",

libs/resources/RespCommandsInfo.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,30 @@
11951195
}
11961196
]
11971197
},
1198+
{
1199+
"Command": "DELIFGREATER",
1200+
"Name": "DELIFGREATER",
1201+
"Arity": 2,
1202+
"FirstKey": 1,
1203+
"LastKey": 1,
1204+
"Step": 1,
1205+
"AclCategories": "Slow, String, Write",
1206+
"KeySpecifications": [
1207+
{
1208+
"BeginSearch": {
1209+
"TypeDiscriminator": "BeginSearchIndex",
1210+
"Index": 1
1211+
},
1212+
"FindKeys": {
1213+
"TypeDiscriminator": "FindKeysRange",
1214+
"LastKey": 0,
1215+
"KeyStep": 1,
1216+
"Limit": 0
1217+
},
1218+
"Flags": "RM, Delete"
1219+
}
1220+
]
1221+
},
11981222
{
11991223
"Command": "DISCARD",
12001224
"Name": "DISCARD",

libs/server/API/GarnetApi.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ public GarnetStatus SET(ref SpanByte key, ref RawStringInput input, ref SpanByte
130130
public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input)
131131
=> storageSession.SET_Conditional(ref key, ref input, ref context);
132132

133+
/// <inheritdoc />
134+
public GarnetStatus DEL_Conditional(ref SpanByte key, ref RawStringInput input)
135+
=> storageSession.DEL_Conditional(ref key, ref input, ref context);
136+
133137
/// <inheritdoc />
134138
public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output)
135139
=> storageSession.SET_Conditional(ref key, ref input, ref output, ref context);

libs/server/API/IGarnetApi.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
3838
/// </summary>
3939
GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input);
4040

41+
/// <summary>
42+
/// DEL Conditional
43+
/// </summary>
44+
GarnetStatus DEL_Conditional(ref SpanByte key, ref RawStringInput input);
45+
4146
/// <summary>
4247
/// SET Conditional
4348
/// </summary>

libs/server/Resp/BasicEtagCommands.cs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,41 @@ private bool NetworkGETIFNOTMATCH<TGarnetApi>(ref TGarnetApi storageApi)
7777
return true;
7878
}
7979

80+
/// <summary>
81+
/// DELIFGREATER key etag
82+
/// </summary>
83+
/// <typeparam name="TGarnetApi"></typeparam>
84+
/// <param name="storageApi"></param>
85+
/// <returns></returns>
86+
private bool NetworkDELIFGREATER<TGarnetApi>(ref TGarnetApi storageApi)
87+
where TGarnetApi : IGarnetApi
88+
{
89+
if (parseState.Count != 2)
90+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.DELIFGREATER));
91+
92+
SpanByte key = parseState.GetArgSliceByRef(0).SpanByte;
93+
if (!parseState.TryGetLong(1, out long givenEtag) || givenEtag < 0)
94+
{
95+
while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_INVALID_ETAG, ref dcurr, dend))
96+
SendAndReset();
97+
return true;
98+
}
99+
100+
// Conditional delete is not natively supported for records in the stable region.
101+
// To achieve this, we use a conditional DEL command to gain RMW (Read-Modify-Write) access, enabling deletion based on conditions.
102+
103+
RawStringInput input = new RawStringInput(RespCommand.DELIFGREATER, ref parseState, startIdx: 1);
104+
input.header.SetWithEtagFlag();
105+
106+
GarnetStatus status = storageApi.DEL_Conditional(ref key, ref input);
107+
108+
int keysDeleted = status == GarnetStatus.OK ? 1 : 0;
109+
110+
while (!RespWriteUtils.TryWriteInt32(keysDeleted, ref dcurr, dend))
111+
SendAndReset();
112+
113+
return true;
114+
}
80115

81116
/// <summary>
82117
/// SETIFMATCH key val etag [EX|PX] [expiry] [NOGET]
@@ -88,9 +123,7 @@ private bool NetworkGETIFNOTMATCH<TGarnetApi>(ref TGarnetApi storageApi)
88123
/// <returns></returns>
89124
private bool NetworkSETIFMATCH<TGarnetApi>(ref TGarnetApi storageApi)
90125
where TGarnetApi : IGarnetApi
91-
{
92-
return NetworkSetETagConditional(RespCommand.SETIFMATCH, ref storageApi);
93-
}
126+
=> NetworkSetETagConditional(RespCommand.SETIFMATCH, ref storageApi);
94127

95128

96129
/// <summary>
@@ -103,9 +136,7 @@ private bool NetworkSETIFMATCH<TGarnetApi>(ref TGarnetApi storageApi)
103136
/// <returns></returns>
104137
private bool NetworkSETIFGREATER<TGarnetApi>(ref TGarnetApi storageApi)
105138
where TGarnetApi : IGarnetApi
106-
{
107-
return NetworkSetETagConditional(RespCommand.SETIFGREATER, ref storageApi);
108-
}
139+
=> NetworkSetETagConditional(RespCommand.SETIFGREATER, ref storageApi);
109140

110141
private bool NetworkSetETagConditional<TGarnetApi>(RespCommand cmd, ref TGarnetApi storageApi)
111142
where TGarnetApi : IGarnetApi
@@ -115,7 +146,7 @@ private bool NetworkSetETagConditional<TGarnetApi>(RespCommand cmd, ref TGarnetA
115146

116147
if (parseState.Count < 3 || parseState.Count > 6)
117148
{
118-
return AbortWithWrongNumberOfArguments(nameof(cmd));
149+
return AbortWithWrongNumberOfArguments(cmd.ToString());
119150
}
120151

121152
int expiry = 0;

libs/server/Resp/CmdStrings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ static partial class CmdStrings
155155
public static ReadOnlySpan<byte> GETIFNOTMATCH => "GETIFNOTMATCH"u8;
156156
public static ReadOnlySpan<byte> SETIFMATCH => "SETIFMATCH"u8;
157157
public static ReadOnlySpan<byte> SETIFGREATER => "SETIFGREATER"u8;
158+
public static ReadOnlySpan<byte> DELIFGREATER => "DELIFGREATER"u8;
158159
public static ReadOnlySpan<byte> FIELDS => "FIELDS"u8;
159160
public static ReadOnlySpan<byte> MEMBERS => "MEMBERS"u8;
160161
public static ReadOnlySpan<byte> TIMEOUT => "TIMEOUT"u8;

libs/server/Resp/Parser/RespCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public enum RespCommand : ushort
117117
DECR,
118118
DECRBY,
119119
DEL,
120+
DELIFGREATER,
120121
EXPIRE,
121122
EXPIREAT,
122123
FLUSHALL,
@@ -2563,6 +2564,10 @@ private RespCommand SlowParseCommand(ReadOnlySpan<byte> command, ref int count,
25632564
{
25642565
return RespCommand.GETIFNOTMATCH;
25652566
}
2567+
else if (command.SequenceEqual(CmdStrings.DELIFGREATER))
2568+
{
2569+
return RespCommand.DELIFGREATER;
2570+
}
25662571

25672572
// If this command name was not known to the slow pass, we are out of options and the command is unknown.
25682573
return RespCommand.INVALID;

libs/server/Resp/RespServerSession.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ private bool ProcessOtherCommands<TGarnetApi>(RespCommand command, ref TGarnetAp
946946
RespCommand.GETIFNOTMATCH => NetworkGETIFNOTMATCH(ref storageApi),
947947
RespCommand.SETIFMATCH => NetworkSETIFMATCH(ref storageApi),
948948
RespCommand.SETIFGREATER => NetworkSETIFGREATER(ref storageApi),
949+
RespCommand.DELIFGREATER => NetworkDELIFGREATER(ref storageApi),
949950

950951
_ => Process(command, ref storageApi)
951952
};

libs/server/Storage/Functions/MainStore/RMWMethods.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp
2727
case RespCommand.PEXPIREAT:
2828
case RespCommand.GETDEL:
2929
case RespCommand.GETEX:
30+
case RespCommand.DELIFGREATER:
3031
return false;
3132
case RespCommand.SETEXXX:
3233
// when called withetag all output needs to be placed on the buffer
@@ -355,9 +356,15 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re
355356
EtagState.ResetState(ref functionsState.etagState);
356357
// Nothing is set because being in this block means NX was already violated
357358
return true;
359+
case RespCommand.DELIFGREATER:
360+
long etagFromClient = input.parseState.GetLong(0);
361+
rmwInfo.Action = etagFromClient > functionsState.etagState.etag ? RMWAction.ExpireAndStop : RMWAction.CancelOperation;
362+
EtagState.ResetState(ref functionsState.etagState);
363+
return false;
364+
358365
case RespCommand.SETIFGREATER:
359366
case RespCommand.SETIFMATCH:
360-
long etagFromClient = input.parseState.GetLong(1);
367+
etagFromClient = input.parseState.GetLong(1);
361368
// in IFMATCH we check for equality, in IFGREATER we are checking for sent etag being strictly greater
362369
int comparisonResult = etagFromClient.CompareTo(functionsState.etagState.etag);
363370
int expectedResult = cmd is RespCommand.SETIFMATCH ? 0 : 1;
@@ -901,8 +908,22 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB
901908
{
902909
switch (input.header.cmd)
903910
{
911+
case RespCommand.DELIFGREATER:
912+
if (rmwInfo.RecordInfo.ETag)
913+
EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref oldValue);
914+
long etagFromClient = input.parseState.GetLong(0);
915+
if (etagFromClient <= functionsState.etagState.etag)
916+
{
917+
EtagState.ResetState(ref functionsState.etagState);
918+
return false;
919+
}
920+
921+
return true;
904922
case RespCommand.SETIFGREATER:
905923
case RespCommand.SETIFMATCH:
924+
if (rmwInfo.RecordInfo.ETag)
925+
EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref oldValue);
926+
906927
long etagToCheckWith = input.parseState.GetLong(1);
907928

908929
// in IFMATCH we check for equality, in IFGREATER we are checking for sent etag being strictly greater
@@ -936,7 +957,6 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB
936957

937958
EtagState.ResetState(ref functionsState.etagState);
938959
return false;
939-
940960
case RespCommand.SETEXNX:
941961
// Expired data, return false immediately
942962
// ExpireAndResume ensures that we set as new value, since it does not exist
@@ -1026,10 +1046,16 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
10261046

10271047
switch (cmd)
10281048
{
1049+
case RespCommand.DELIFGREATER:
1050+
// NCU has already checked for making sure the etag is greater than the existing etag by this point
1051+
long etagFromClient = input.parseState.GetLong(0);
1052+
rmwInfo.Action = RMWAction.ExpireAndStop;
1053+
EtagState.ResetState(ref functionsState.etagState);
1054+
return false;
1055+
10291056
case RespCommand.SETIFGREATER:
10301057
case RespCommand.SETIFMATCH:
10311058
// By now the comparison for etag against existing etag has already been done in NeedCopyUpdate
1032-
10331059
shouldUpdateEtag = true;
10341060
// Copy input to value
10351061
Span<byte> dest = newValue.AsSpan(EtagConstants.EtagSize);
@@ -1048,7 +1074,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte
10481074
newValue.ExtraMetadata = input.arg1;
10491075
}
10501076

1051-
long etagFromClient = input.parseState.GetLong(1);
1077+
etagFromClient = input.parseState.GetLong(1);
10521078

10531079
functionsState.etagState.etag = etagFromClient;
10541080

libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,17 +224,18 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input)
224224
return sizeof(int) + newValue.Length + offset + t.MetadataSize + functionsState.etagState.etagOffsetForVarlen;
225225
return sizeof(int) + t.Length;
226226

227-
case RespCommand.GETDEL:
228-
// No additional allocation needed.
229-
break;
230-
231227
case RespCommand.GETEX:
232228
return sizeof(int) + t.LengthWithoutMetadata + (input.arg1 > 0 ? sizeof(long) : 0);
233229

234230
case RespCommand.APPEND:
235231
var valueLength = input.parseState.GetArgSliceByRef(0).Length;
236232
return sizeof(int) + t.Length + valueLength;
237233

234+
case RespCommand.GETDEL:
235+
case RespCommand.DELIFGREATER:
236+
// Min allocation (only metadata) needed since this is going to be used for tombstoning anyway.
237+
return sizeof(int);
238+
238239
default:
239240
if (cmd > RespCommandExtensions.LastValidCommand)
240241
{

libs/server/Storage/Session/MainStore/MainStoreOps.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,38 @@ public unsafe GarnetStatus SET_Conditional<TContext>(ref SpanByte key, ref RawSt
377377
}
378378
}
379379

380+
381+
public unsafe GarnetStatus DEL_Conditional<TContext>(ref SpanByte key, ref RawStringInput input, ref TContext context)
382+
where TContext : ITsavoriteContext<SpanByte, SpanByte, RawStringInput, SpanByteAndMemory, long, MainSessionFunctions, MainStoreFunctions, MainStoreAllocator>
383+
{
384+
Debug.Assert(input.header.cmd == RespCommand.DELIFGREATER);
385+
386+
byte* pbOutput = stackalloc byte[8];
387+
var o = new SpanByteAndMemory(pbOutput, 8);
388+
var status = context.RMW(ref key, ref input, ref o);
389+
390+
if (status.IsPending)
391+
{
392+
StartPendingMetrics();
393+
CompletePendingForSession(ref status, ref o, ref context);
394+
StopPendingMetrics();
395+
}
396+
397+
// Deletions in RMW are done by expiring the record, hence we use expiration as the indicator of success.
398+
if (status.Expired)
399+
{
400+
incr_session_found();
401+
return GarnetStatus.OK;
402+
}
403+
else
404+
{
405+
if (status.NotFound)
406+
incr_session_notfound();
407+
408+
return GarnetStatus.NOTFOUND;
409+
}
410+
}
411+
380412
public unsafe GarnetStatus SET_Conditional<TContext>(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, ref TContext context)
381413
where TContext : ITsavoriteContext<SpanByte, SpanByte, RawStringInput, SpanByteAndMemory, long, MainSessionFunctions, MainStoreFunctions, MainStoreAllocator>
382414
{

libs/server/Transaction/TxnKeyManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ internal int GetKeys(RespCommand command, int inputCount, out ReadOnlySpan<byte>
160160
RespCommand.SETEXNX => SingleKey(1, false, LockType.Exclusive),
161161
RespCommand.SETEXXX => SingleKey(1, false, LockType.Exclusive),
162162
RespCommand.DEL => ListKeys(inputCount, false, LockType.Exclusive),
163+
RespCommand.DELIFGREATER => SingleKey(1, false, LockType.Exclusive),
163164
RespCommand.EXISTS => SingleKey(1, false, LockType.Shared),
164165
RespCommand.RENAME => SingleKey(1, false, LockType.Exclusive),
165166
RespCommand.INCR => SingleKey(1, false, LockType.Exclusive),

playground/CommandInfoUpdater/GarnetCommandsDocs.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,28 @@
215215
}
216216
]
217217
},
218+
{
219+
"Command": "DELIFGREATER",
220+
"Name": "DELIFGREATER",
221+
"Summary": "Deletes a key only if the provided Etag is strictly greater than the existing Etag for the key.",
222+
"Group": "Generic",
223+
"Complexity": "O(1)",
224+
"Arguments": [
225+
{
226+
"TypeDiscriminator": "RespCommandKeyArgument",
227+
"Name": "KEY",
228+
"DisplayText": "key",
229+
"Type": "Key",
230+
"KeySpecIndex": 0
231+
},
232+
{
233+
"TypeDiscriminator": "RespCommandBasicArgument",
234+
"Name": "ETAG",
235+
"DisplayText": "etag",
236+
"Type": "Integer"
237+
}
238+
]
239+
},
218240
{
219241
"Command": "FORCEGC",
220242
"Name": "FORCEGC",

0 commit comments

Comments
 (0)