Skip to content

Commit ec5a46a

Browse files
Vijay-NirmalTalZaccaiyrajas
authored
[Compatibility] Adding RENAMENX command (#661)
* Inital changes (Doesn't build) * New changes from rename * Temp changes for calling SET_Conditional * Added basic test * Updated CommandsInfo * Changes done for non object keys * Final commit * Added proper comments * Code format fix * Added ACL test for renamenx * Corrected the function name * Fix for review comments * Fixed code styling issue --------- Co-authored-by: Tal Zaccai <[email protected]> Co-authored-by: Yoganand Rajasekaran <[email protected]>
1 parent 0786f48 commit ec5a46a

File tree

12 files changed

+472
-32
lines changed

12 files changed

+472
-32
lines changed

libs/server/API/GarnetApi.cs

+4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ public GarnetStatus APPEND(ArgSlice key, ArgSlice value, ref ArgSlice output)
154154
/// <inheritdoc />
155155
public GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All)
156156
=> storageSession.RENAME(oldKey, newKey, storeType);
157+
158+
/// <inheritdoc />
159+
public GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, StoreType storeType = StoreType.All)
160+
=> storageSession.RENAMENX(oldKey, newKey, storeType, out result);
157161
#endregion
158162

159163
#region EXISTS

libs/server/API/IGarnetApi.cs

+10
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
118118
/// <param name="storeType"></param>
119119
/// <returns></returns>
120120
GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All);
121+
122+
/// <summary>
123+
/// Renames key to newkey if newkey does not yet exist. It returns an error when key does not exist.
124+
/// </summary>
125+
/// <param name="oldKey">The old key to be renamed.</param>
126+
/// <param name="newKey">The new key name.</param>
127+
/// <param name="result">The result of the operation.</param>
128+
/// <param name="storeType">The type of store to perform the operation on.</param>
129+
/// <returns></returns>
130+
GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, StoreType storeType = StoreType.All);
121131
#endregion
122132

123133
#region EXISTS

libs/server/Resp/KeyAdminCommands.cs

+31
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,37 @@ private bool NetworkRENAME<TGarnetApi>(ref TGarnetApi storageApi)
4040
return true;
4141
}
4242

43+
/// <summary>
44+
/// TryRENAMENX
45+
/// </summary>
46+
private bool NetworkRENAMENX<TGarnetApi>(ref TGarnetApi storageApi)
47+
where TGarnetApi : IGarnetApi
48+
{
49+
if (parseState.Count != 2)
50+
{
51+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.RENAMENX));
52+
}
53+
54+
var oldKeySlice = parseState.GetArgSliceByRef(0);
55+
var newKeySlice = parseState.GetArgSliceByRef(1);
56+
var status = storageApi.RENAMENX(oldKeySlice, newKeySlice, out var result);
57+
58+
if (status == GarnetStatus.OK)
59+
{
60+
// Integer reply: 1 if key was renamed to newkey.
61+
// Integer reply: 0 if newkey already exists.
62+
while (!RespWriteUtils.WriteInteger(result, ref dcurr, dend))
63+
SendAndReset();
64+
}
65+
else
66+
{
67+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_NOSUCHKEY, ref dcurr, dend))
68+
SendAndReset();
69+
}
70+
71+
return true;
72+
}
73+
4374
/// <summary>
4475
/// GETDEL command processor
4576
/// </summary>

libs/server/Resp/Parser/RespCommand.cs

+2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public enum RespCommand : byte
117117
PFMERGE,
118118
PSETEX,
119119
RENAME,
120+
RENAMENX,
120121
RPOP,
121122
RPOPLPUSH,
122123
RPUSH,
@@ -620,6 +621,7 @@ private RespCommand FastParseCommand(out int count)
620621
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("INCRBY\r\n"u8) => RespCommand.INCRBY,
621622
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("DECRBY\r\n"u8) => RespCommand.DECRBY,
622623
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("RENAME\r\n"u8) => RespCommand.RENAME,
624+
(2 << 4) | 8 when lastWord == MemoryMarshal.Read<ulong>("NAMENX\r\n"u8) && *(ushort*)(ptr + 8) == MemoryMarshal.Read<ushort>("RE"u8) => RespCommand.RENAMENX,
623625
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("GETBIT\r\n"u8) => RespCommand.GETBIT,
624626
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("APPEND\r\n"u8) => RespCommand.APPEND,
625627
(2 << 4) | 7 when lastWord == MemoryMarshal.Read<ulong>("UBLISH\r\n"u8) && ptr[8] == 'P' => RespCommand.PUBLISH,

libs/server/Resp/RespCommandsInfo.json

+43
Original file line numberDiff line numberDiff line change
@@ -3573,6 +3573,49 @@
35733573
],
35743574
"SubCommands": null
35753575
},
3576+
{
3577+
"Command": "RENAMENX",
3578+
"Name": "RENAMENX",
3579+
"IsInternal": false,
3580+
"Arity": 3,
3581+
"Flags": "Fast, Write",
3582+
"FirstKey": 1,
3583+
"LastKey": 2,
3584+
"Step": 1,
3585+
"AclCategories": "Fast, KeySpace, Write",
3586+
"Tips": null,
3587+
"KeySpecifications": [
3588+
{
3589+
"BeginSearch": {
3590+
"TypeDiscriminator": "BeginSearchIndex",
3591+
"Index": 1
3592+
},
3593+
"FindKeys": {
3594+
"TypeDiscriminator": "FindKeysRange",
3595+
"LastKey": 0,
3596+
"KeyStep": 1,
3597+
"Limit": 0
3598+
},
3599+
"Notes": null,
3600+
"Flags": "RW, Access, Delete"
3601+
},
3602+
{
3603+
"BeginSearch": {
3604+
"TypeDiscriminator": "BeginSearchIndex",
3605+
"Index": 2
3606+
},
3607+
"FindKeys": {
3608+
"TypeDiscriminator": "FindKeysRange",
3609+
"LastKey": 0,
3610+
"KeyStep": 1,
3611+
"Limit": 0
3612+
},
3613+
"Notes": null,
3614+
"Flags": "OW, Insert"
3615+
}
3616+
],
3617+
"SubCommands": null
3618+
},
35763619
{
35773620
"Command": "REPLICAOF",
35783621
"Name": "REPLICAOF",

libs/server/Resp/RespServerSession.cs

+1
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ private bool ProcessBasicCommands<TGarnetApi>(RespCommand cmd, ref TGarnetApi st
512512
RespCommand.SETEXNX => NetworkSETEXNX(ref storageApi),
513513
RespCommand.DEL => NetworkDEL(ref storageApi),
514514
RespCommand.RENAME => NetworkRENAME(ref storageApi),
515+
RespCommand.RENAMENX => NetworkRENAMENX(ref storageApi),
515516
RespCommand.EXISTS => NetworkEXISTS(ref storageApi),
516517
RespCommand.EXPIRE => NetworkEXPIRE(RespCommand.EXPIRE, ref storageApi),
517518
RespCommand.PEXPIRE => NetworkEXPIRE(RespCommand.PEXPIRE, ref storageApi),

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

+89-27
Original file line numberDiff line numberDiff line change
@@ -521,12 +521,33 @@ public GarnetStatus DELETE<TContext, TObjectContext>(byte[] key, StoreType store
521521
}
522522

523523
public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType)
524+
{
525+
return RENAME(oldKeySlice, newKeySlice, storeType, false, out _);
526+
}
527+
528+
/// <summary>
529+
/// Renames key to newkey if newkey does not yet exist. It returns an error when key does not exist.
530+
/// </summary>
531+
/// <param name="oldKeySlice">The old key to be renamed.</param>
532+
/// <param name="newKeySlice">The new key name.</param>
533+
/// <param name="storeType">The type of store to perform the operation on.</param>
534+
/// <returns></returns>
535+
public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, out int result)
536+
{
537+
return RENAME(oldKeySlice, newKeySlice, storeType, true, out result);
538+
}
539+
540+
private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool isNX, out int result)
524541
{
525542
GarnetStatus returnStatus = GarnetStatus.NOTFOUND;
543+
result = -1;
526544

527545
// If same name check return early.
528546
if (oldKeySlice.ReadOnlySpan.SequenceEqual(newKeySlice.ReadOnlySpan))
547+
{
548+
result = 1;
529549
return GarnetStatus.OK;
550+
}
530551

531552
bool createTransaction = false;
532553
if (txnManager.state != TxnState.Running)
@@ -570,23 +591,68 @@ public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, St
570591
// If the key has an expiration, set the new key with the expiration
571592
if (expireTimeMs > 0)
572593
{
573-
SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context);
594+
if (isNX)
595+
{
596+
// Move payload forward to make space for RespInputHeader and Metadata
597+
var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength));
598+
var setValueSpan = setValue.SpanByte;
599+
var setValuePtr = setValueSpan.ToPointerWithMetadata();
600+
setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks;
601+
((RespInputHeader*)(setValuePtr + sizeof(long)))->cmd = RespCommand.SETEXNX;
602+
((RespInputHeader*)(setValuePtr + sizeof(long)))->flags = 0;
603+
var newKey = newKeySlice.SpanByte;
604+
var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context);
605+
606+
// For SET NX `NOTFOUND` means the operation succeeded
607+
result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0;
608+
returnStatus = GarnetStatus.OK;
609+
}
610+
else
611+
{
612+
SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context);
613+
}
574614
}
575615
else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key
576616
{
577-
SpanByte newKey = newKeySlice.SpanByte;
578-
var value = SpanByte.FromPinnedPointer(ptrVal, headerLength);
579-
SET(ref newKey, ref value, ref context);
617+
if (isNX)
618+
{
619+
// Move payload forward to make space for RespInputHeader
620+
var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size, new ArgSlice(ptrVal, headerLength));
621+
var setValueSpan = setValue.SpanByte;
622+
var setValuePtr = setValueSpan.ToPointerWithMetadata();
623+
((RespInputHeader*)setValuePtr)->cmd = RespCommand.SETEXNX;
624+
((RespInputHeader*)setValuePtr)->flags = 0;
625+
var newKey = newKeySlice.SpanByte;
626+
var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context);
627+
628+
// For SET NX `NOTFOUND` means the operation succeeded
629+
result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0;
630+
returnStatus = GarnetStatus.OK;
631+
}
632+
else
633+
{
634+
SpanByte newKey = newKeySlice.SpanByte;
635+
var value = SpanByte.FromPinnedPointer(ptrVal, headerLength);
636+
SET(ref newKey, ref value, ref context);
637+
}
580638
}
581639

582640
expireSpan.Memory.Dispose();
583641
memoryHandle.Dispose();
584642
o.Memory.Dispose();
585643

586-
// Delete the old key
587-
DELETE(ref oldKey, StoreType.Main, ref context, ref objectContext);
644+
// Delete the old key only when SET NX succeeded
645+
if (isNX && result == 1)
646+
{
647+
DELETE(ref oldKey, StoreType.Main, ref context, ref objectContext);
648+
}
649+
else if (!isNX)
650+
{
651+
// Delete the old key
652+
DELETE(ref oldKey, StoreType.Main, ref context, ref objectContext);
588653

589-
returnStatus = GarnetStatus.OK;
654+
returnStatus = GarnetStatus.OK;
655+
}
590656
}
591657
}
592658
}
@@ -618,32 +684,29 @@ public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, St
618684
var valObj = value.garnetObject;
619685
byte[] newKeyArray = newKeySlice.ToArray();
620686

621-
var expireSpan = new SpanByteAndMemory();
622-
var ttlStatus = TTL(ref oldKey, StoreType.Object, ref expireSpan, ref context, ref objectContext, true);
623-
624-
if (ttlStatus == GarnetStatus.OK && !expireSpan.IsSpanByte)
687+
returnStatus = GarnetStatus.OK;
688+
var canSetAndDelete = true;
689+
if (isNX)
625690
{
626-
using var expireMemoryHandle = expireSpan.Memory.Memory.Pin();
627-
var expirePtrVal = (byte*)expireMemoryHandle.Pointer;
628-
RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _);
629-
expireSpan.Memory.Dispose();
691+
// Not using EXISTS method to avoid new allocation of Array for key
692+
var getNewStatus = GET(newKeyArray, out _, ref objectContext);
693+
canSetAndDelete = getNewStatus == GarnetStatus.NOTFOUND;
694+
}
630695

631-
if (expireTimeMs > 0)
632-
{
633-
SET(newKeyArray, valObj, ref objectContext);
634-
EXPIRE(newKeySlice, TimeSpan.FromMilliseconds(expireTimeMs), out _, StoreType.Object, ExpireOption.None, ref context, ref objectContext, true);
635-
}
636-
else if (expireTimeMs == -1) // Its possible to have expire as 0 or -2, in those cases we don't SET the new key
637-
{
638-
SET(newKeyArray, valObj, ref objectContext);
639-
}
696+
if (canSetAndDelete)
697+
{
698+
// valObj already has expiration time, so no need to write expiration logic here
699+
SET(newKeyArray, valObj, ref objectContext);
640700

641701
// Delete the old key
642702
DELETE(oldKeyArray, StoreType.Object, ref context, ref objectContext);
643703

644-
returnStatus = GarnetStatus.OK;
704+
result = 1;
705+
}
706+
else
707+
{
708+
result = 0;
645709
}
646-
647710
}
648711
}
649712
finally
@@ -652,7 +715,6 @@ public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, St
652715
txnManager.Commit(true);
653716
}
654717
}
655-
656718
return returnStatus;
657719
}
658720

playground/CommandInfoUpdater/SupportedCommand.cs

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public class SupportedCommand
194194
new("READONLY", RespCommand.READONLY),
195195
new("READWRITE", RespCommand.READWRITE),
196196
new("RENAME", RespCommand.RENAME),
197+
new("RENAMENX", RespCommand.RENAMENX),
197198
new("REPLICAOF", RespCommand.REPLICAOF),
198199
new("RPOP", RespCommand.RPOP),
199200
new("RPOPLPUSH", RespCommand.RPOPLPUSH),

test/Garnet.test/Resp/ACL/RespCommandTests.cs

+29-2
Original file line numberDiff line numberDiff line change
@@ -4278,10 +4278,10 @@ public async Task RenameACLsAsync()
42784278
{
42794279
await CheckCommandsAsync(
42804280
"RENAME",
4281-
[DoPTTLAsync]
4281+
[DoRENAMEAsync]
42824282
);
42834283

4284-
static async Task DoPTTLAsync(GarnetClient client)
4284+
static async Task DoRENAMEAsync(GarnetClient client)
42854285
{
42864286
try
42874287
{
@@ -4300,6 +4300,33 @@ static async Task DoPTTLAsync(GarnetClient client)
43004300
}
43014301
}
43024302

4303+
[Test]
4304+
public async Task RenameNxACLsAsync()
4305+
{
4306+
await CheckCommandsAsync(
4307+
"RENAMENX",
4308+
[DoRENAMENXAsync]
4309+
);
4310+
4311+
static async Task DoRENAMENXAsync(GarnetClient client)
4312+
{
4313+
try
4314+
{
4315+
await client.ExecuteForStringResultAsync("RENAMENX", ["foo", "bar"]);
4316+
Assert.Fail("Shouldn't succeed, key doesn't exist");
4317+
}
4318+
catch (Exception e)
4319+
{
4320+
if (e.Message == "ERR no such key")
4321+
{
4322+
return;
4323+
}
4324+
4325+
throw;
4326+
}
4327+
}
4328+
}
4329+
43034330
[Test]
43044331
public async Task ReplicaOfACLsAsync()
43054332
{

0 commit comments

Comments
 (0)