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] Added COMMAND GETKEYS and GETKEYSANDFLAGS command #888

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
44 changes: 44 additions & 0 deletions libs/resources/RespCommandsDocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,50 @@
"ArgumentFlags": "Optional, Multiple"
}
]
},
{
"Command": "COMMAND_GETKEYS",
"Name": "COMMAND|GETKEYS",
"Summary": "Extracts the key names from an arbitrary command.",
"Group": "Server",
"Complexity": "O(N) where N is the number of arguments to the command",
"Arguments": [
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "COMMAND",
"DisplayText": "command",
"Type": "String"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "ARG",
"DisplayText": "arg",
"Type": "String",
"ArgumentFlags": "Optional, Multiple"
}
]
},
{
"Command": "COMMAND_GETKEYSANDFLAGS",
"Name": "COMMAND|GETKEYSANDFLAGS",
"Summary": "Extracts the key names and access flags for an arbitrary command.",
"Group": "Server",
"Complexity": "O(N) where N is the number of arguments to the command",
"Arguments": [
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "COMMAND",
"DisplayText": "command",
"Type": "String"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "ARG",
"DisplayText": "arg",
"Type": "String",
"ArgumentFlags": "Optional, Multiple"
}
]
}
]
},
Expand Down
14 changes: 14 additions & 0 deletions libs/resources/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,20 @@
"Tips": [
"nondeterministic_output_order"
]
},
{
"Command": "COMMAND_GETKEYS",
"Name": "COMMAND|GETKEYS",
"Arity": -3,
"Flags": "Loading, Stale",
"AclCategories": "Connection, Slow"
},
{
"Command": "COMMAND_GETKEYSANDFLAGS",
"Name": "COMMAND|GETKEYSANDFLAGS",
"Arity": -3,
"Flags": "Loading, Stale",
"AclCategories": "Connection, Slow"
}
]
},
Expand Down
225 changes: 225 additions & 0 deletions libs/server/Resp/BasicCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
Expand Down Expand Up @@ -1149,6 +1150,230 @@ private bool NetworkCOMMAND_INFO()
return true;
}

/// <summary>
/// Processes COMMAND GETKEYS subcommand.
/// </summary>
private bool NetworkCOMMAND_GETKEYS()
{
if (parseState.Count == 0)
{
return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMAND_GETKEYS));
}

var cmdName = parseState.GetString(0).ToUpperInvariant();
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for ToUpperInvariant(), the TryGet... methods already take care of that.

bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I think includeSubCommands should be false (I don't believe sub-commands should have key specs - please verify)

storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo);

if (!cmdFound)
{
return AbortWithErrorMessage(CmdStrings.RESP_INVALID_COMMAND_SPECIFIED);
}

if (cmdInfo.KeySpecifications == null || cmdInfo.KeySpecifications.Length == 0)
{
return AbortWithErrorMessage(CmdStrings.RESP_COMMAND_HAS_NO_KEY_ARGS);
}

var keyList = new List<byte[]>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we have to allocate a byte[] for each key? The keys already exist in the parse state.

foreach (var spec in cmdInfo.KeySpecifications)
{
ExtractKeys(spec, keyList);
}

while (!RespWriteUtils.WriteArrayLength(keyList.Count, ref dcurr, dend))
SendAndReset();

foreach (var key in keyList)
{
while (!RespWriteUtils.WriteBulkString(key, ref dcurr, dend))
SendAndReset();
}

return true;
}

/// <summary>
/// Processes COMMAND GETKEYSANDFLAGS subcommand.
/// </summary>
private bool NetworkCOMMAND_GETKEYSANDFLAGS()
{
if (parseState.Count == 0)
{
return AbortWithWrongNumberOfArguments(nameof(RespCommand.COMMAND_GETKEYSANDFLAGS));
}

var cmdName = parseState.GetString(0).ToUpperInvariant();
Copy link
Contributor

Choose a reason for hiding this comment

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

See same comments as in GETKEYS

bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) ||
storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo);

if (!cmdFound)
{
return AbortWithErrorMessage(CmdStrings.RESP_INVALID_COMMAND_SPECIFIED);
}

if (cmdInfo.KeySpecifications == null || cmdInfo.KeySpecifications.Length == 0)
{
return AbortWithErrorMessage(CmdStrings.RESP_COMMAND_HAS_NO_KEY_ARGS);
}

var keyList = new List<byte[]>();
var flagsList = new List<string[]>();

foreach (var spec in cmdInfo.KeySpecifications)
{
var keyCount = keyList.Count;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: just for clarity, should probably be named prevKeyCount

ExtractKeys(spec, keyList);
var flags = EnumUtils.GetEnumDescriptions(spec.Flags);
Copy link
Contributor

Choose a reason for hiding this comment

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

spec.respFormatFlags should already contain this info, you can create a public getter than returns this field.

for (int i = keyCount; i < keyList.Count; i++)
{
flagsList.Add(flags);
}
}

while (!RespWriteUtils.WriteArrayLength(keyList.Count, ref dcurr, dend))
SendAndReset();

for (int i = 0; i < keyList.Count; i++)
{
while (!RespWriteUtils.WriteArrayLength(2, ref dcurr, dend))
SendAndReset();

while (!RespWriteUtils.WriteBulkString(keyList[i], ref dcurr, dend))
SendAndReset();

while (!RespWriteUtils.WriteArrayLength(flagsList[i].Length, ref dcurr, dend))
SendAndReset();

foreach (var flag in flagsList[i])
{
while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(flag), ref dcurr, dend))
SendAndReset();
}
}

return true;
}

private void ExtractKeys(RespCommandKeySpecification spec, List<byte[]> keyList)
Copy link
Contributor

Choose a reason for hiding this comment

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

What I would probably do is have this as an extension method to SessionParseState (e.g. TryGetKeysFromKeySpecs) that takes an array of RespCommandKeySpecifications.
Then the internal implementation of extracting the keys from a single key spec could a class method inside RespCommandKeySpecification.

{
int startIndex = 0;

if (spec.BeginSearch is BeginSearchIndex bsIndex)
Copy link
Contributor

Choose a reason for hiding this comment

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

You can take advantage of the OO design here and handle each class-specific scenario in the class implementation itself.

{
startIndex = bsIndex.Index;
if (startIndex < 0)
startIndex = parseState.Count + startIndex;
}
else if (spec.BeginSearch is BeginSearchKeyword bsKeyword)
{
// Handle negative StartFrom by converting to positive index from end
int searchStartIndex = bsKeyword.StartFrom;
if (searchStartIndex < 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can probably avoid having 2 loops that do the same thing here, just have a condition for the index if the order is descending (this path is probably not as performance-critical).

{
// Convert negative index to positive from end
searchStartIndex = parseState.Count + searchStartIndex;

// Search backwards from the calculated start position for the keyword
for (int i = searchStartIndex; i >= 0; i--)
{
if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword)))
Copy link
Contributor

Choose a reason for hiding this comment

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

You can store Encoding.ASCII.GetBytes(bsKeyword.Keyword) prior to the loop instead of re-evaluating it each time.

{
startIndex = i + 1;
break;
}
}
}
else
{
// Search forwards from start position for the keyword
for (int i = searchStartIndex; i < parseState.Count; i++)
{
if (parseState.GetArgSliceByRef(i).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(Encoding.ASCII.GetBytes(bsKeyword.Keyword)))
{
startIndex = i + 1;
break;
}
}
}
}

if (startIndex < 0 || startIndex >= parseState.Count)
return;

if (spec.FindKeys is FindKeysRange range)
{
int lastKey;
if (range.LastKey < 0)
{
// For negative LastKey, calculate limit based on the factor
int availableArgs = parseState.Count - startIndex;
int limitFactor = range.Limit <= 1 ? availableArgs : availableArgs / range.Limit;

// Calculate available slots based on keyStep
int slotsAvailable = (limitFactor + range.KeyStep - 1) / range.KeyStep;
lastKey = startIndex + (slotsAvailable * range.KeyStep) - range.KeyStep;
}
else
{
lastKey = Math.Min(startIndex + range.LastKey, parseState.Count - 1);
}

for (int i = startIndex; i <= lastKey; i += range.KeyStep)
{
if (i < parseState.Count)
{
var value = parseState.GetArgSliceByRef(i).ToArray();
if (value.Length != 0)
{
keyList.Add(value);
}
}
}
}
else if (spec.FindKeys is FindKeysKeyNum keyNum)
{
int numKeys = 0;
int firstKey = startIndex + keyNum.FirstKey;

// Handle negative FirstKey
if (keyNum.FirstKey < 0)
firstKey = parseState.Count + keyNum.FirstKey;

// Get number of keys from the KeyNumIdx
if (keyNum.KeyNumIdx >= 0)
{
var keyNumPos = startIndex + keyNum.KeyNumIdx;
if (keyNumPos < parseState.Count && parseState.TryGetInt(keyNumPos, out var count))
{
numKeys = count;
}
}
else
{
// Negative KeyNumIdx means count from the end
var keyNumPos = parseState.Count + keyNum.KeyNumIdx;
if (keyNumPos >= 0 && parseState.TryGetInt(keyNumPos, out var count))
{
numKeys = count;
}
}

// Extract keys based on numKeys, firstKey, and keyStep
if (numKeys > 0 && firstKey >= 0)
{
for (int i = 0; i < numKeys && firstKey + i * keyNum.KeyStep < parseState.Count; i++)
{
var keyIndex = firstKey + i * keyNum.KeyStep;
var value = parseState.GetArgSliceByRef(keyIndex).ToArray();
if (value.Length != 0)
{
keyList.Add(value);
}
}
}
}
}

private bool NetworkECHO()
{
if (parseState.Count != 1)
Expand Down
4 changes: 4 additions & 0 deletions libs/server/Resp/CmdStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> info => "info"u8;
public static ReadOnlySpan<byte> DOCS => "DOCS"u8;
public static ReadOnlySpan<byte> docs => "docs"u8;
public static ReadOnlySpan<byte> GETKEYS => "GETKEYS"u8;
public static ReadOnlySpan<byte> GETKEYSANDFLAGS => "GETKEYSANDFLAGS"u8;
public static ReadOnlySpan<byte> COMMAND => "COMMAND"u8;
public static ReadOnlySpan<byte> LATENCY => "LATENCY"u8;
public static ReadOnlySpan<byte> CLUSTER => "CLUSTER"u8;
Expand Down Expand Up @@ -215,6 +217,8 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8;
public static ReadOnlySpan<byte> RESP_ERR_INVALID_BITFIELD_TYPE => "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"u8;
public static ReadOnlySpan<byte> RESP_ERR_SCRIPT_FLUSH_OPTIONS => "ERR SCRIPT FLUSH only support SYNC|ASYNC option"u8;
public static ReadOnlySpan<byte> RESP_INVALID_COMMAND_SPECIFIED => "Invalid command specified"u8;
public static ReadOnlySpan<byte> RESP_COMMAND_HAS_NO_KEY_ARGS => "The command has no key arguments"u8;

/// <summary>
/// Response string templates
Expand Down
14 changes: 14 additions & 0 deletions libs/server/Resp/Parser/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ public enum RespCommand : ushort
COMMAND_COUNT,
COMMAND_DOCS,
COMMAND_INFO,
COMMAND_GETKEYS,
COMMAND_GETKEYSANDFLAGS,

MEMORY,
// MEMORY_USAGE is a read-only command, so moved up
Expand Down Expand Up @@ -375,6 +377,8 @@ public static class RespCommandExtensions
RespCommand.COMMAND_COUNT,
RespCommand.COMMAND_DOCS,
RespCommand.COMMAND_INFO,
RespCommand.COMMAND_GETKEYS,
RespCommand.COMMAND_GETKEYSANDFLAGS,
RespCommand.MEMORY_USAGE,
// Config
RespCommand.CONFIG_GET,
Expand Down Expand Up @@ -1729,6 +1733,16 @@ private RespCommand SlowParseCommand(ref int count, ref ReadOnlySpan<byte> speci
{
return RespCommand.COMMAND_DOCS;
}

if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYS))
{
return RespCommand.COMMAND_GETKEYS;
}

if (subCommand.EqualsUpperCaseSpanIgnoringCase(CmdStrings.GETKEYSANDFLAGS))
{
return RespCommand.COMMAND_GETKEYSANDFLAGS;
}
}
else if (command.SequenceEqual(CmdStrings.PING))
{
Expand Down
2 changes: 2 additions & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ private bool ProcessOtherCommands<TGarnetApi>(RespCommand command, ref TGarnetAp
RespCommand.COMMAND_COUNT => NetworkCOMMAND_COUNT(),
RespCommand.COMMAND_DOCS => NetworkCOMMAND_DOCS(),
RespCommand.COMMAND_INFO => NetworkCOMMAND_INFO(),
RespCommand.COMMAND_GETKEYS => NetworkCOMMAND_GETKEYS(),
RespCommand.COMMAND_GETKEYSANDFLAGS => NetworkCOMMAND_GETKEYSANDFLAGS(),
RespCommand.ECHO => NetworkECHO(),
RespCommand.HELLO => NetworkHELLO(),
RespCommand.TIME => NetworkTIME(),
Expand Down
2 changes: 1 addition & 1 deletion playground/CommandInfoUpdater/CommandDocsUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ private static IReadOnlyDictionary<string, RespCommandDocs> GetUpdatedCommandsDo
}

// Update commands docs with commands to add
foreach (var command in commandsToAdd.Keys)
foreach (var command in commandsToAdd.Keys.Where(x => x.Command.StartsWith("COMMAND")))
Copy link
Contributor

Choose a reason for hiding this comment

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

Mistake?

{
RespCommandDocs baseCommandDocs;
List<RespCommandDocs> updatedSubCommandsDocs;
Expand Down
2 changes: 2 additions & 0 deletions playground/CommandInfoUpdater/SupportedCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ public class SupportedCommand
new("COMMAND|INFO", RespCommand.COMMAND_INFO),
new("COMMAND|COUNT", RespCommand.COMMAND_COUNT),
new("COMMAND|DOCS", RespCommand.COMMAND_DOCS),
new("COMMAND|GETKEYS", RespCommand.COMMAND_GETKEYS),
new("COMMAND|GETKEYSANDFLAGS", RespCommand.COMMAND_GETKEYSANDFLAGS),
]),
new("COMMITAOF", RespCommand.COMMITAOF),
new("CONFIG", RespCommand.CONFIG,
Expand Down
Loading
Loading