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 12 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",
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here about running CommentInfoUpdater - #864 (comment)

"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
90 changes: 90 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.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -1170,6 +1171,95 @@ 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);
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);
}

parseState.TryExtractKeysFromSpecs(cmdInfo.KeySpecifications, out var keys);


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

foreach (var key in keys)
{
while (!RespWriteUtils.WriteBulkString(key.Span, 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);
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);
}

parseState.TryExtractKeyandFlagsFromSpecs(cmdInfo.KeySpecifications, out var keys, out var flags);

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

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

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

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

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

return true;
}

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 @@ -228,6 +230,8 @@ static partial class CmdStrings
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_ERR_LENGTH_AND_INDEXES => "If you want both the length and indexes, please just use IDX."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 @@ -270,6 +270,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 @@ -379,6 +381,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 @@ -1736,6 +1740,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
1 change: 1 addition & 0 deletions libs/server/Resp/Parser/SessionParseState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Garnet.common;
using Garnet.common.Parsing;
using Tsavorite.core;
Expand Down
89 changes: 89 additions & 0 deletions libs/server/Resp/Parser/SessionParseStateExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System.Collections.Generic;

namespace Garnet.server
{
/// <summary>
/// Extension methods for the SessionParseState struct.
/// </summary>
internal static class SessionParseStateExtension
Copy link
Contributor

Choose a reason for hiding this comment

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

These methods can be added to the existing Garnet.server/SessionParseStateExtensions.cs

{
/// <summary>
/// Tries to extract keys from the key specifications in the given RespCommandsInfo.
/// </summary>
/// <param name="state">The SessionParseState instance.</param>
/// <param name="keySpecs">The RespCommandKeySpecification array contains the key specification</param>
/// <param name="keys">The list to store extracted keys.</param>
/// <returns>True if keys were successfully extracted, otherwise false.</returns>
internal static bool TryExtractKeysFromSpecs(this ref SessionParseState state, RespCommandKeySpecification[] keySpecs, out List<ArgSlice> keys)
{
keys = new();

foreach (var spec in keySpecs)
{
if (!ExtractKeysFromSpec(ref state, keys, spec))
{
return false;
}
}

return true;
}

internal static bool TryExtractKeyandFlagsFromSpecs(this ref SessionParseState state, RespCommandKeySpecification[] keySpecs, out List<ArgSlice> keys, out List<string[]> flags)
Copy link
Contributor

Choose a reason for hiding this comment

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

naming: TryExtractKeysAndFlagsFromSpecs

Copy link
Contributor

Choose a reason for hiding this comment

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

Missing XML comment

{
keys = new();
flags = new();

foreach (var spec in keySpecs)
{
var prevKeyCount = keys.Count;
if (!ExtractKeysFromSpec(ref state, keys, spec))
{
return false;
}

var keyFlags = spec.RespFormatFlags;
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 return a slice of this array instead of creating this list

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't because we logic to skip and take like skip 1, take 1, skip 1 take 1.... or skip 2, take 1.....

for (int i = prevKeyCount; i < keys.Count; i++)
{
flags.Add(keyFlags);
}
}

return true;
}

private static bool ExtractKeysFromSpec(ref SessionParseState state, List<ArgSlice> keys, RespCommandKeySpecification spec)
{
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.

Here too, you can have an abstract method called GetStartIndex (or something similar) in BeginSearchKeySpecMethodBase that you can implement for each of the different types.

{
startIndex = bsIndex.GetIndex(ref state);
}
else if (spec.BeginSearch is BeginSearchKeyword bsKeyword)
{
if (!bsKeyword.TryGetStartFrom(ref state, out startIndex))
{
return false;
}
}

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

if (spec.FindKeys is FindKeysRange range)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of doing this type matching you can have ExtractKeys defined as an abstract method in FindKeysKeySpecMethodBase

{
range.ExtractKeys(ref state, startIndex, keys);
}
else if (spec.FindKeys is FindKeysKeyNum keyNum)
{
keyNum.ExtractKeys(ref state, startIndex, keys);
}

return true;
}
}
}
Loading
Loading