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 RENAMENX command #661

Open
wants to merge 15 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
4 changes: 4 additions & 0 deletions libs/server/API/GarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ public GarnetStatus APPEND(ArgSlice key, ArgSlice value, ref ArgSlice output)
/// <inheritdoc />
public GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All)
=> storageSession.RENAME(oldKey, newKey, storeType);

/// <inheritdoc />
public GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All)
=> storageSession.RENAMENX(oldKey, newKey, storeType);
#endregion

#region EXISTS
Expand Down
13 changes: 13 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
/// <param name="storeType"></param>
/// <returns></returns>
GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All);

/// <summary>
/// Renames key to newkey if newkey does not yet exist. It returns an error when key does not exist.
/// </summary>
/// <param name="oldKey">The old key to be renamed.</param>
/// <param name="newKey">The new key name.</param>
/// <param name="storeType">The type of store to perform the operation on.</param>
/// <returns>
/// GarnetStatus OK means rename operation is successful
/// GarnetStatus NOTFOUND means old key does not exists
/// GarnetStatus MOVED means new key already exists
/// </returns>
GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All);
#endregion

#region EXISTS
Expand Down
40 changes: 40 additions & 0 deletions libs/server/Resp/KeyAdminCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ private bool NetworkRENAME<TGarnetApi>(ref TGarnetApi storageApi)
return true;
}

/// <summary>
/// TryRENAMENX
/// </summary>
private bool NetworkRENAMENX<TGarnetApi>(ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
if (parseState.Count != 2)
{
return AbortWithWrongNumberOfArguments(nameof(RespCommand.RENAMENX));
}

var oldKeySlice = parseState.GetArgSliceByRef(0);
var newKeySlice = parseState.GetArgSliceByRef(1);
var status = storageApi.RENAMENX(oldKeySlice, newKeySlice);
Copy link
Contributor

Choose a reason for hiding this comment

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

GarnetStatus is not meant to be interpreted this way, you should pass an output parameter to this method and write the appropriate output to it.


// Integer reply: 1 if key was renamed to newkey.
// Integer reply: 0 if newkey already exists.
switch (status)
{
// GarnetStatus OK means new key already exists
case GarnetStatus.OK:
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_1, ref dcurr, dend))
SendAndReset();
break;

// GarnetStatus NOTFOUND means remane operation is successful
case GarnetStatus.NOTFOUND:
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_NOSUCHKEY, ref dcurr, dend))
SendAndReset();
break;

case GarnetStatus.MOVED:
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_0, ref dcurr, dend))
SendAndReset();
break;
}

return true;
}

/// <summary>
/// GETDEL command processor
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions libs/server/Resp/Parser/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public enum RespCommand : byte
PFMERGE,
PSETEX,
RENAME,
RENAMENX,
RPOP,
RPOPLPUSH,
RPUSH,
Expand Down Expand Up @@ -620,6 +621,7 @@ private RespCommand FastParseCommand(out int count)
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("INCRBY\r\n"u8) => RespCommand.INCRBY,
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("DECRBY\r\n"u8) => RespCommand.DECRBY,
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("RENAME\r\n"u8) => RespCommand.RENAME,
(2 << 4) | 8 when lastWord == MemoryMarshal.Read<ulong>("NAMENX\r\n"u8) => RespCommand.RENAMENX,
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to read the 'RE' prefix as well

(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("GETBIT\r\n"u8) => RespCommand.GETBIT,
(2 << 4) | 6 when lastWord == MemoryMarshal.Read<ulong>("APPEND\r\n"u8) => RespCommand.APPEND,
(2 << 4) | 7 when lastWord == MemoryMarshal.Read<ulong>("UBLISH\r\n"u8) && ptr[8] == 'P' => RespCommand.PUBLISH,
Expand Down
43 changes: 43 additions & 0 deletions libs/server/Resp/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -3573,6 +3573,49 @@
],
"SubCommands": null
},
{
"Command": "RENAMENX",
"Name": "RENAMENX",
"IsInternal": false,
"Arity": 3,
"Flags": "Fast, Write",
"FirstKey": 1,
"LastKey": 2,
"Step": 1,
"AclCategories": "Fast, KeySpace, Write",
"Tips": null,
"KeySpecifications": [
{
"BeginSearch": {
"TypeDiscriminator": "BeginSearchIndex",
"Index": 1
},
"FindKeys": {
"TypeDiscriminator": "FindKeysRange",
"LastKey": 0,
"KeyStep": 1,
"Limit": 0
},
"Notes": null,
"Flags": "RW, Access, Delete"
},
{
"BeginSearch": {
"TypeDiscriminator": "BeginSearchIndex",
"Index": 2
},
"FindKeys": {
"TypeDiscriminator": "FindKeysRange",
"LastKey": 0,
"KeyStep": 1,
"Limit": 0
},
"Notes": null,
"Flags": "OW, Insert"
}
],
"SubCommands": null
},
{
"Command": "REPLICAOF",
"Name": "REPLICAOF",
Expand Down
1 change: 1 addition & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ private bool ProcessBasicCommands<TGarnetApi>(RespCommand cmd, ref TGarnetApi st
RespCommand.SETEXNX => NetworkSETEXNX(ref storageApi),
RespCommand.DEL => NetworkDEL(ref storageApi),
RespCommand.RENAME => NetworkRENAME(ref storageApi),
RespCommand.RENAMENX => NetworkRENAMENX(ref storageApi),
RespCommand.EXISTS => NetworkEXISTS(ref storageApi),
RespCommand.EXPIRE => NetworkEXPIRE(RespCommand.EXPIRE, ref storageApi),
RespCommand.PEXPIRE => NetworkEXPIRE(RespCommand.PEXPIRE, ref storageApi),
Expand Down
154 changes: 146 additions & 8 deletions libs/server/Storage/Session/MainStore/MainStoreOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -618,32 +618,171 @@ public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, St
var valObj = value.garnetObject;
byte[] newKeyArray = newKeySlice.ToArray();

// valObj already has expiration time, so no need to write expiration logic here
SET(newKeyArray, valObj, ref objectContext);

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

returnStatus = GarnetStatus.OK;
}
}
finally
{
if (createTransaction)
txnManager.Commit(true);
}
}

return returnStatus;
}

/// <summary>
/// Renames key to newkey if newkey does not yet exist. It returns an error when key does not exist.
/// </summary>
/// <param name="oldKeySlice">The old key to be renamed.</param>
/// <param name="newKeySlice">The new key name.</param>
/// <param name="storeType">The type of store to perform the operation on.</param>
/// <returns>
/// GarnetStatus OK means rename operation is successful
/// GarnetStatus NOTFOUND means old key does not exists
/// GarnetStatus MOVED means new key already exists
/// </returns>
public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType)
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is majorly similar to RENAME, I would suggest adding a bool parameter to the RENAME method to specify if rename should happen only if key doesn't exist, instead of copying all this code (as any future change to this would need to happen in multiple locations).

{
GarnetStatus returnStatus = GarnetStatus.NOTFOUND;

// If same name check return early.
if (oldKeySlice.ReadOnlySpan.SequenceEqual(newKeySlice.ReadOnlySpan))
return GarnetStatus.OK;

bool createTransaction = false;
if (txnManager.state != TxnState.Running)
{
createTransaction = true;
txnManager.SaveKeyEntryToLock(oldKeySlice, false, LockType.Exclusive);
txnManager.SaveKeyEntryToLock(newKeySlice, false, LockType.Exclusive);
txnManager.Run(true);
}

var context = txnManager.LockableContext;
var objectContext = txnManager.ObjectStoreLockableContext;
SpanByte oldKey = oldKeySlice.SpanByte;

if (storeType == StoreType.Main || storeType == StoreType.All)
{
try
{
SpanByte input = default;
var o = new SpanByteAndMemory();
var status = GET(ref oldKey, ref input, ref o, ref context);

if (status == GarnetStatus.OK)
{
Debug.Assert(!o.IsSpanByte);
var memoryHandle = o.Memory.Memory.Pin();
var ptrVal = (byte*)memoryHandle.Pointer;

RespReadUtils.ReadUnsignedLengthHeader(out var headerLength, ref ptrVal, ptrVal + o.Length);

// Find expiration time of the old key
var expireSpan = new SpanByteAndMemory();
var ttlStatus = TTL(ref oldKey, StoreType.Object, ref expireSpan, ref context, ref objectContext, true);
var ttlStatus = TTL(ref oldKey, storeType, ref expireSpan, ref context, ref objectContext, true);

if (ttlStatus == GarnetStatus.OK && !expireSpan.IsSpanByte)
{
using var expireMemoryHandle = expireSpan.Memory.Memory.Pin();
var expirePtrVal = (byte*)expireMemoryHandle.Pointer;
RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _);
expireSpan.Memory.Dispose();

// If the key has an expiration, set the new key with the expiration
if (expireTimeMs > 0)
{
SET(newKeyArray, valObj, ref objectContext);
EXPIRE(newKeySlice, TimeSpan.FromMilliseconds(expireTimeMs), out _, StoreType.Object, ExpireOption.None, ref context, ref objectContext, true);
// Move payload forward to make space for RespInputHeader and Metadata
var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength));
var setValueSpan = setValue.SpanByte;
var setValuePtr = setValueSpan.ToPointerWithMetadata();
setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks;
((RespInputHeader*)(setValuePtr + sizeof(long)))->cmd = RespCommand.SETEXNX;
((RespInputHeader*)(setValuePtr + sizeof(long)))->flags = 0;
var newKey = newKeySlice.SpanByte;
var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context);

// For SET NX `NOTFOUND` means the operation succeeded
returnStatus = setStatus == GarnetStatus.NOTFOUND ? GarnetStatus.OK : GarnetStatus.MOVED;
}
else if (expireTimeMs == -1) // Its possible to have expire as 0 or -2, in those cases we don't SET the new key
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
{
// Move payload forward to make space for RespInputHeader
var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size, new ArgSlice(ptrVal, headerLength));
var setValueSpan = setValue.SpanByte;
var setValuePtr = setValueSpan.ToPointerWithMetadata();
((RespInputHeader*)setValuePtr)->cmd = RespCommand.SETEXNX;
((RespInputHeader*)setValuePtr)->flags = 0;
var newKey = newKeySlice.SpanByte;
var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context);

// For SET NX `NOTFOUND` means the operation succeeded
returnStatus = setStatus == GarnetStatus.NOTFOUND ? GarnetStatus.OK : GarnetStatus.MOVED;
}

expireSpan.Memory.Dispose();
memoryHandle.Dispose();
o.Memory.Dispose();

// Delete the old key only when SET NX succeeded
if (returnStatus == GarnetStatus.OK)
{
SET(newKeyArray, valObj, ref objectContext);
DELETE(ref oldKey, StoreType.Main, ref context, ref objectContext);
}
}
}
}
finally
{
if (createTransaction)
txnManager.Commit(true);
}
}

if ((storeType == StoreType.Object || storeType == StoreType.All) && !objectStoreBasicContext.IsNull)
{
createTransaction = false;
if (txnManager.state != TxnState.Running)
{
txnManager.SaveKeyEntryToLock(oldKeySlice, true, LockType.Exclusive);
txnManager.SaveKeyEntryToLock(newKeySlice, true, LockType.Exclusive);
txnManager.Run(true);
createTransaction = true;
}

try
{
byte[] oldKeyArray = oldKeySlice.ToArray();
var status = GET(oldKeyArray, out var value, ref objectContext);

if (status == GarnetStatus.OK)
{
var valObj = value.garnetObject;
byte[] newKeyArray = newKeySlice.ToArray();

// Not using EXISTS method to avoid new allocation of Array for key
var getNewStatus = GET(newKeyArray, out _, ref objectContext);

if (getNewStatus == GarnetStatus.NOTFOUND)
{
// valObj already has expiration time, so no need to write expiration logic here
SET(newKeyArray, valObj, ref objectContext);

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

returnStatus = GarnetStatus.OK;
}

else
{
returnStatus = GarnetStatus.MOVED;
}
}
}
finally
Expand All @@ -652,7 +791,6 @@ public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, St
txnManager.Commit(true);
}
}

return returnStatus;
}

Expand Down
1 change: 1 addition & 0 deletions playground/CommandInfoUpdater/SupportedCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public class SupportedCommand
new("READONLY", RespCommand.READONLY),
new("READWRITE", RespCommand.READWRITE),
new("RENAME", RespCommand.RENAME),
new("RENAMENX", RespCommand.RENAMENX),
new("REPLICAOF", RespCommand.REPLICAOF),
new("RPOP", RespCommand.RPOP),
new("RPOPLPUSH", RespCommand.RPOPLPUSH),
Expand Down
31 changes: 29 additions & 2 deletions test/Garnet.test/Resp/ACL/RespCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4278,10 +4278,10 @@ public async Task RenameACLsAsync()
{
await CheckCommandsAsync(
"RENAME",
[DoPTTLAsync]
[DoRENAMEAsync]
);

static async Task DoPTTLAsync(GarnetClient client)
static async Task DoRENAMEAsync(GarnetClient client)
{
try
{
Expand All @@ -4300,6 +4300,33 @@ static async Task DoPTTLAsync(GarnetClient client)
}
}

[Test]
public async Task RenameNxACLsAsync()
{
await CheckCommandsAsync(
"RENAMENX",
[DoRENAMENXAsync]
);

static async Task DoRENAMENXAsync(GarnetClient client)
{
try
{
await client.ExecuteForStringResultAsync("RENAMENX", ["foo", "bar"]);
Assert.Fail("Shouldn't succeed, key doesn't exist");
}
catch (Exception e)
{
if (e.Message == "ERR no such key")
{
return;
}

throw;
}
}
}

[Test]
public async Task ReplicaOfACLsAsync()
{
Expand Down
Loading