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

Merged
merged 16 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
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",
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
"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();
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
bool cmdFound = RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, true, true, logger) ||
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
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[]>();
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
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();
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
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;
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
ExtractKeys(spec, keyList);
var flags = EnumUtils.GetEnumDescriptions(spec.Flags);
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
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)
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
{
int startIndex = 0;

if (spec.BeginSearch is BeginSearchIndex bsIndex)
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
{
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)
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
{
// 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)))
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
{
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")))
TalZaccai marked this conversation as resolved.
Show resolved Hide resolved
{
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