From 7f1184d60bc3332596b8fc2f4bc223eef398c390 Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Thu, 9 May 2024 11:35:48 -0600 Subject: [PATCH] Supported RESP Commands Info (#287) * wip * WIP * fixes * wip * wip * wip * Added command info for custom commands * Added command count * Moved StreamProvider to common & added RespCommandsInfoProvider * Error handling, logging + bugfix * Added some tests * bugfixes * Added RespSerializableBase * wip * wip * Adding logic to CommandInfoUpdater tool * Finishing CommandInfoUpdater app * Moving LightClientRequest to common + moving CommandInfoUpdater under playground * running dotnet format * Adding some comments * some cleanup * Added comments to CommandInfoUpdater * dotnet format * Small bugfixes, added more supported commands * Some cleanup & small changes * Addressing comments + fixing some namings * Added test to verify coverage of command info, added some more missing commands info * Added WATCHOS, WATCHMS as internal commands * dotnet format * removing redis references from CommandInfoUpdater, adding "force" option * Updating RespCommandsInfo.json * Addressing comments * formatting * Update playground/CommandInfoUpdater/SupportedCommand.cs Co-authored-by: Lukas Maas * Update libs/server/Resp/RespCommandInfoParser.cs Co-authored-by: Lukas Maas * Clean up singleton comments in RespCommandInfoParser.cs * Fixing naming + checking output from calls to RespReadUtils * Some small fixes * Update test/Garnet.test/RespCommandTests.cs Co-authored-by: Lukas Maas * Update playground/CommandInfoUpdater/CommandInfoUpdater.cs Co-authored-by: Lukas Maas * Update playground/CommandInfoUpdater/CommandInfoUpdater.cs Co-authored-by: Lukas Maas * Update playground/CommandInfoUpdater/CommandInfoUpdater.cs Co-authored-by: Lukas Maas * Disabling Nullable in CommandInfoUpdater.csproj + Fixing thread-safety issue in RespCommandsInfo initialization * Adding documentation * Docs fix --------- Co-authored-by: Lukas Maas Co-authored-by: Badrish Chandramouli --- Garnet.sln | 11 + benchmark/Resp.benchmark/BenchUtils.cs | 7 +- .../Resp.benchmark/Resp.benchmark.csproj | 7 +- libs/common/EnumUtils.cs | 113 + libs/common/Garnet.common.csproj | 5 + .../common}/LightClientRequest.cs | 3 +- libs/{host => common}/StreamProvider.cs | 32 +- libs/host/Configuration/ConfigProviders.cs | 1 + libs/host/ServerSettingsManager.cs | 9 +- libs/server/Custom/CustomCommandManager.cs | 27 +- .../Custom/CustomCommandRegistration.cs | 15 +- libs/server/Garnet.server.csproj | 4 + libs/server/Resp/AdminCommands.cs | 30 - libs/server/Resp/BasicCommands.cs | 221 +- libs/server/Resp/CmdStrings.cs | 4 + libs/server/Resp/IRespSerializable.cs | 17 + libs/server/Resp/RespCommand.cs | 1 - libs/server/Resp/RespCommandInfoFlags.cs | 110 + libs/server/Resp/RespCommandInfoParser.cs | 514 ++ .../Resp/RespCommandKeySpecification.cs | 576 ++ libs/server/Resp/RespCommandsInfo.cs | 507 +- libs/server/Resp/RespCommandsInfo.json | 4770 +++++++++++++++++ libs/server/Resp/RespCommandsInfoProvider.cs | 146 + libs/server/Resp/RespInfo.cs | 51 - libs/server/Resp/RespServerSession.cs | 1 + libs/server/Servers/RegisterApi.cs | 15 +- libs/server/Transaction/TxnRespCommands.cs | 10 +- main/GarnetServer/CustomRespCommandsInfo.json | 72 + main/GarnetServer/GarnetServer.csproj | 3 + main/GarnetServer/Program.cs | 24 +- playground/ClusterStress/ClusterStress.csproj | 2 +- .../CommandInfoUpdater/CommandInfoUpdater.cs | 468 ++ .../CommandInfoUpdater.csproj | 27 + .../GarnetCommandsInfo.json | 162 + playground/CommandInfoUpdater/Options.cs | 25 + playground/CommandInfoUpdater/Program.cs | 74 + .../CommandInfoUpdater/SupportedCommand.cs | 277 + samples/MetricsMonitor/Configuration.cs | 7 +- .../ClusterRedirectTests.cs | 1 + .../Garnet.test.cluster.csproj | 1 - test/Garnet.test/Garnet.test.csproj | 9 +- test/Garnet.test/GarnetServerConfigTests.cs | 3 +- test/Garnet.test/RespAofTests.cs | 9 +- test/Garnet.test/RespCommandTests.cs | 272 + test/Garnet.test/RespCustomCommandTests.cs | 44 +- test/Garnet.test/RespScanCommandsTests.cs | 7 +- test/Garnet.test/RespSortedSetTests.cs | 1 + test/Garnet.test/TestUtils.cs | 54 +- website/docs/commands/analytics.md | 15 + website/docs/commands/api-compatibility.md | 8 +- website/docs/commands/server.md | 36 +- website/docs/dev/garnet-api.md | 60 +- 52 files changed, 8435 insertions(+), 433 deletions(-) create mode 100644 libs/common/EnumUtils.cs rename {test/Garnet.test => libs/common}/LightClientRequest.cs (99%) rename libs/{host => common}/StreamProvider.cs (89%) create mode 100644 libs/server/Resp/IRespSerializable.cs create mode 100644 libs/server/Resp/RespCommandInfoFlags.cs create mode 100644 libs/server/Resp/RespCommandInfoParser.cs create mode 100644 libs/server/Resp/RespCommandKeySpecification.cs create mode 100644 libs/server/Resp/RespCommandsInfo.json create mode 100644 libs/server/Resp/RespCommandsInfoProvider.cs delete mode 100644 libs/server/Resp/RespInfo.cs create mode 100644 main/GarnetServer/CustomRespCommandsInfo.json create mode 100644 playground/CommandInfoUpdater/CommandInfoUpdater.cs create mode 100644 playground/CommandInfoUpdater/CommandInfoUpdater.csproj create mode 100644 playground/CommandInfoUpdater/GarnetCommandsInfo.json create mode 100644 playground/CommandInfoUpdater/Options.cs create mode 100644 playground/CommandInfoUpdater/Program.cs create mode 100644 playground/CommandInfoUpdater/SupportedCommand.cs create mode 100644 test/Garnet.test/RespCommandTests.cs diff --git a/Garnet.sln b/Garnet.sln index 070cdc4bd2..a42c0ca7a1 100644 --- a/Garnet.sln +++ b/Garnet.sln @@ -91,6 +91,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Embedded.perftest", "playgr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BDN.benchmark", "benchmark\BDN.benchmark\BDN.benchmark.csproj", "{9F6E4734-6341-4A9C-A7FF-636A39D8BEAD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandInfoUpdater", "playground\CommandInfoUpdater\CommandInfoUpdater.csproj", "{9BE474A2-1547-43AC-B4F2-FB48A01FA995}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,14 @@ Global {9F6E4734-6341-4A9C-A7FF-636A39D8BEAD}.Release|Any CPU.Build.0 = Release|Any CPU {9F6E4734-6341-4A9C-A7FF-636A39D8BEAD}.Release|x64.ActiveCfg = Release|Any CPU {9F6E4734-6341-4A9C-A7FF-636A39D8BEAD}.Release|x64.Build.0 = Release|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Debug|x64.ActiveCfg = Debug|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Debug|x64.Build.0 = Debug|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|Any CPU.Build.0 = Release|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|x64.ActiveCfg = Release|Any CPU + {9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -286,6 +296,7 @@ Global {8941A05C-099B-45AC-A7BF-F0E226BD59A8} = {69A71E2C-00E3-42F3-854E-BE157A24834E} {5BEDAC1F-6458-4EBA-8174-EC06B07F2132} = {69A71E2C-00E3-42F3-854E-BE157A24834E} {9F6E4734-6341-4A9C-A7FF-636A39D8BEAD} = {346A5A53-51E4-4A75-B7E6-491D950382CE} + {9BE474A2-1547-43AC-B4F2-FB48A01FA995} = {69A71E2C-00E3-42F3-854E-BE157A24834E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2C02C405-4798-41CA-AF98-61EDFEF6772E} diff --git a/benchmark/Resp.benchmark/BenchUtils.cs b/benchmark/Resp.benchmark/BenchUtils.cs index 3318a9abe7..f90d9ff802 100644 --- a/benchmark/Resp.benchmark/BenchUtils.cs +++ b/benchmark/Resp.benchmark/BenchUtils.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Net.Security; using System.Security.Cryptography.X509Certificates; @@ -31,10 +32,14 @@ public static SslClientAuthenticationOptions GetTlsOptions(string tlsHost, strin public static ConfigurationOptions GetConfig(string address, int port = default, bool allowAdmin = false, bool useTLS = false, string tlsHost = null) { + var commands = RespCommandsInfo.TryGetRespCommandNames(out var cmds) + ? new HashSet(cmds) + : new HashSet(); + var configOptions = new ConfigurationOptions { EndPoints = { { address, port }, }, - CommandMap = CommandMap.Create(RespInfo.GetCommands()), + CommandMap = CommandMap.Create(commands), ConnectTimeout = 100_000, SyncTimeout = 100_000, AllowAdmin = allowAdmin, diff --git a/benchmark/Resp.benchmark/Resp.benchmark.csproj b/benchmark/Resp.benchmark/Resp.benchmark.csproj index 0a6687647e..b8d159b8ac 100644 --- a/benchmark/Resp.benchmark/Resp.benchmark.csproj +++ b/benchmark/Resp.benchmark/Resp.benchmark.csproj @@ -6,10 +6,6 @@ true - - - - PreserveNewest @@ -25,7 +21,8 @@ - + + \ No newline at end of file diff --git a/libs/common/EnumUtils.cs b/libs/common/EnumUtils.cs new file mode 100644 index 0000000000..07adc9f380 --- /dev/null +++ b/libs/common/EnumUtils.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace Garnet.common +{ + /// + /// Utilities for enums + /// + public static class EnumUtils + { + private static readonly Dictionary> EnumNameToDescriptionCache = new(); + private static readonly Dictionary>> EnumDescriptionToNameCache = new(); + + /// + /// Gets a mapping between an enum's string value to its description, for each of the enum's values + /// + /// Enum type + /// A dictionary mapping between the enum's string value to its description + public static IDictionary GetEnumNameToDescription() where T : Enum + { + // Check if mapping is already in the cache. If not, add it to the cache. + if (!EnumNameToDescriptionCache.ContainsKey(typeof(T))) + AddTypeToCache(); + + return EnumNameToDescriptionCache[typeof(T)]; + } + + /// + /// If enum does not have the 'Flags' attribute, gets an array of size 1 with the description of the enum's value. + /// If no description exists, returns the ToString() value of the input value. + /// If enum has the 'Flags' attribute, gets an array with all the descriptions of the flags which are turned on in the input value. + /// If no description exists, returns the ToString() value of the flag. + /// + /// Enum type + /// Enum value + /// Array of descriptions + public static string[] GetEnumDescriptions(T value) where T : Enum + { + var nameToDesc = GetEnumNameToDescription(); + return value.ToString().Split(',').Select(f => nameToDesc.ContainsKey(f.Trim()) ? nameToDesc[f.Trim()] : f).ToArray(); + } + + /// + /// Gets an enum's values based on the description attribute + /// + /// Enum type + /// Enum description + /// Enum values + /// True if matched more than one value successfully + public static bool TryParseEnumsFromDescription(string strVal, out IEnumerable vals) where T : struct, Enum + { + vals = new List(); + + if (!EnumDescriptionToNameCache.ContainsKey(typeof(T))) + AddTypeToCache(); + + if (!EnumDescriptionToNameCache[typeof(T)].ContainsKey(strVal)) + return false; + + foreach (var enumName in EnumDescriptionToNameCache[typeof(T)][strVal]) + { + if (Enum.TryParse(enumName, out T enumVal)) + { + ((List)vals).Add(enumVal); + } + } + + return ((List)vals).Count > 0; + } + + /// + /// Gets an enum's value based on its description attribute + /// If more than one values match the same description, returns the first one + /// + /// Enum type + /// Enum description + /// Enum value + /// True if successful + public static bool TryParseEnumFromDescription(string strVal, out T val) where T : struct, Enum + { + var isSuccessful = TryParseEnumsFromDescription(strVal, out IEnumerable vals); + val = isSuccessful ? vals.First() : default; + return isSuccessful; + } + + + private static void AddTypeToCache() + { + var valToDesc = new Dictionary(); + var descToVals = new Dictionary>(); + + foreach (var flagFieldInfo in typeof(T).GetFields()) + { + var descAttr = (DescriptionAttribute)flagFieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault(); + if (descAttr != null) + { + valToDesc.Add(flagFieldInfo.Name, descAttr.Description); + if (!descToVals.ContainsKey(descAttr.Description)) + descToVals.Add(descAttr.Description, new List()); + descToVals[descAttr.Description].Add(flagFieldInfo.Name); + } + } + + EnumNameToDescriptionCache.Add(typeof(T), valToDesc); + EnumDescriptionToNameCache.Add(typeof(T), descToVals); + } + } +} \ No newline at end of file diff --git a/libs/common/Garnet.common.csproj b/libs/common/Garnet.common.csproj index 72bd5735fb..f006085359 100644 --- a/libs/common/Garnet.common.csproj +++ b/libs/common/Garnet.common.csproj @@ -12,4 +12,9 @@ + + + + + \ No newline at end of file diff --git a/test/Garnet.test/LightClientRequest.cs b/libs/common/LightClientRequest.cs similarity index 99% rename from test/Garnet.test/LightClientRequest.cs rename to libs/common/LightClientRequest.cs index dc47f8c2ce..c1048b0333 100644 --- a/test/Garnet.test/LightClientRequest.cs +++ b/libs/common/LightClientRequest.cs @@ -4,10 +4,9 @@ using System; using System.Net.Security; using System.Text; -using Garnet.common; using Garnet.networking; -namespace Garnet.test +namespace Garnet.common { public unsafe class LightClientRequest : IDisposable { diff --git a/libs/host/StreamProvider.cs b/libs/common/StreamProvider.cs similarity index 89% rename from libs/host/StreamProvider.cs rename to libs/common/StreamProvider.cs index 97164b168b..05b145b7b9 100644 --- a/libs/host/StreamProvider.cs +++ b/libs/common/StreamProvider.cs @@ -10,9 +10,9 @@ using Tsavorite.core; using Tsavorite.devices; -namespace Garnet +namespace Garnet.common { - internal enum FileLocationType + public enum FileLocationType { Local, AzureStorage, @@ -22,7 +22,7 @@ internal enum FileLocationType /// /// Interface for reading / writing into local / remote files /// - internal interface IStreamProvider + public interface IStreamProvider { /// /// Read data from file specified in path @@ -112,15 +112,16 @@ private static void IOCallback(uint errorCode, uint numBytes, object context) /// /// Provides a StreamProvider instance /// - internal class StreamProviderFactory + public class StreamProviderFactory { /// /// Get a StreamProvider instance /// /// Type of location of files the stream provider reads from / writes to /// Connection string to Azure Storage, if applicable + /// Assembly from which to load the embedded resource, if applicable /// StreamProvider instance - internal static IStreamProvider GetStreamProvider(FileLocationType locationType, string connectionString = null) + public static IStreamProvider GetStreamProvider(FileLocationType locationType, string connectionString = null, Assembly resourceAssembly = null) { switch (locationType) { @@ -131,7 +132,7 @@ internal static IStreamProvider GetStreamProvider(FileLocationType locationType, case FileLocationType.Local: return new LocalFileStreamProvider(); case FileLocationType.EmbeddedResource: - return new EmbeddedResourceStreamProvider(); + return new EmbeddedResourceStreamProvider(resourceAssembly); default: throw new NotImplementedException(); } @@ -199,22 +200,29 @@ protected override long GetBytesToWrite(byte[] bytes, IDevice device) /// internal class EmbeddedResourceStreamProvider : IStreamProvider { + private readonly Assembly assembly; + + public EmbeddedResourceStreamProvider(Assembly assembly) + { + this.assembly = assembly; + } + public Stream Read(string path) { - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(rn => rn.EndsWith(path)); + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(rn => rn.EndsWith($".{path}")); if (resourceName == null) return null; - return Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); + return assembly.GetManifestResourceStream(resourceName); } public void Write(string path, byte[] data) { - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = assembly.GetManifestResourceNames().FirstOrDefault(rn => rn.EndsWith(path)); + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(rn => rn.EndsWith($".{path}")); if (resourceName == null) return; - using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); + using var stream = assembly.GetManifestResourceStream(resourceName); if (stream != null) stream.Write(data, 0, data.Length); } diff --git a/libs/host/Configuration/ConfigProviders.cs b/libs/host/Configuration/ConfigProviders.cs index 6289c2aedb..f3adca4793 100644 --- a/libs/host/Configuration/ConfigProviders.cs +++ b/libs/host/Configuration/ConfigProviders.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using System.Text.Json; +using Garnet.common; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; diff --git a/libs/host/ServerSettingsManager.cs b/libs/host/ServerSettingsManager.cs index 913999db7b..d31717679d 100644 --- a/libs/host/ServerSettingsManager.cs +++ b/libs/host/ServerSettingsManager.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using CommandLine; using CommandLine.Text; +using Garnet.common; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -237,7 +238,9 @@ private static Dictionary GetArgumentNameToValue(Options options /// True if import succeeded private static bool TryImportServerOptions(string path, ConfigFileType configFileType, Options options, ILogger logger, FileLocationType fileLocationType, string connString = null) { - var streamProvider = StreamProviderFactory.GetStreamProvider(fileLocationType, connString); + var assembly = fileLocationType == FileLocationType.EmbeddedResource ? Assembly.GetExecutingAssembly() : null; + + var streamProvider = StreamProviderFactory.GetStreamProvider(fileLocationType, connString, assembly); var configProvider = ConfigProviderFactory.GetConfigProvider(configFileType); using var stream = streamProvider.Read(path); @@ -269,7 +272,9 @@ private static bool TryImportServerOptions(string path, ConfigFileType configFil /// True if export succeeded private static bool TryExportServerOptions(string path, ConfigFileType configFileType, Options options, ILogger logger, FileLocationType fileLocationType, string connString = null) { - var streamProvider = StreamProviderFactory.GetStreamProvider(fileLocationType, connString); + var assembly = fileLocationType == FileLocationType.EmbeddedResource ? Assembly.GetExecutingAssembly() : null; + + var streamProvider = StreamProviderFactory.GetStreamProvider(fileLocationType, connString, assembly); var configProvider = ConfigProviderFactory.GetConfigProvider(configFileType); var exportSucceeded = configProvider.TryExportOptions(path, streamProvider, options, logger); diff --git a/libs/server/Custom/CustomCommandManager.cs b/libs/server/Custom/CustomCommandManager.cs index 07c8a72a35..f4241861b6 100644 --- a/libs/server/Custom/CustomCommandManager.cs +++ b/libs/server/Custom/CustomCommandManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; using System.Threading; namespace Garnet.server @@ -21,6 +22,11 @@ public class CustomCommandManager internal int ObjectTypeId = 0; internal int TransactionProcId = 0; + internal int CustomCommandsInfoCount => this.customCommandsInfo.Count; + internal IEnumerable CustomCommandsInfo => this.customCommandsInfo.Values; + + private readonly Dictionary customCommandsInfo = new(StringComparer.OrdinalIgnoreCase); + /// /// Create new custom command manager /// @@ -31,13 +37,14 @@ public CustomCommandManager() transactionProcMap = new CustomTransaction[MaxRegistrations]; // can increase up to byte.MaxValue } - internal int Register(string name, int numParams, CommandType type, CustomRawStringFunctions customFunctions, long expirationTicks) + internal int Register(string name, int numParams, CommandType type, CustomRawStringFunctions customFunctions, RespCommandsInfo commandInfo, long expirationTicks) { int id = Interlocked.Increment(ref CommandId) - 1; if (id >= MaxRegistrations) throw new Exception("Out of registration space"); commandMap[id] = new CustomCommand(name, (byte)id, 1, numParams, type, customFunctions, expirationTicks); + customCommandsInfo.Add(name, commandInfo); return id; } @@ -66,6 +73,7 @@ internal int RegisterType(CustomObjectFactory factory) } while (objectCommandMap[type] != null); objectCommandMap[type] = new CustomObjectCommandWrapper((byte)type, factory); + return type; } @@ -82,19 +90,21 @@ internal void RegisterType(int objectTypeId, CustomObjectFactory factory) objectCommandMap[objectTypeId] = new CustomObjectCommandWrapper((byte)objectTypeId, factory); } - internal int Register(string name, int numParams, CommandType commandType, int objectTypeId) + internal int Register(string name, int numParams, CommandType commandType, int objectTypeId, RespCommandsInfo commandInfo) { var wrapper = objectCommandMap[objectTypeId]; int subCommand = Interlocked.Increment(ref wrapper.CommandId) - 1; if (subCommand >= byte.MaxValue) throw new Exception("Out of registration space"); + wrapper.commandMap[subCommand] = new CustomObjectCommand(name, (byte)objectTypeId, (byte)subCommand, 1, numParams, commandType, wrapper.factory); + customCommandsInfo.Add(name, commandInfo); return subCommand; } - internal (int objectTypeId, int subCommand) Register(string name, int numParams, CommandType commandType, CustomObjectFactory factory) + internal (int objectTypeId, int subCommand) Register(string name, int numParams, CommandType commandType, CustomObjectFactory factory, RespCommandsInfo commandInfo) { int objectTypeId = -1; for (int i = 0; i < ObjectTypeId; i++) @@ -117,6 +127,8 @@ internal int Register(string name, int numParams, CommandType commandType, int o throw new Exception("Out of registration space"); wrapper.commandMap[subCommand] = new CustomObjectCommand(name, (byte)objectTypeId, (byte)subCommand, 1, numParams, commandType, wrapper.factory); + customCommandsInfo.Add(name, commandInfo); + return (objectTypeId, subCommand); } @@ -163,5 +175,14 @@ internal bool Match(ReadOnlySpan command, out CustomObjectCommand cmd) cmd = null; return false; } + + internal bool TryGetCustomCommandInfo(string cmdName, out RespCommandsInfo respCommandsInfo) + { + respCommandsInfo = default; + if (!this.customCommandsInfo.ContainsKey(cmdName)) return false; + + respCommandsInfo = this.customCommandsInfo[cmdName]; + return true; + } } } \ No newline at end of file diff --git a/libs/server/Custom/CustomCommandRegistration.cs b/libs/server/Custom/CustomCommandRegistration.cs index 7051f62223..34bd7e5396 100644 --- a/libs/server/Custom/CustomCommandRegistration.cs +++ b/libs/server/Custom/CustomCommandRegistration.cs @@ -21,6 +21,11 @@ internal abstract class RegisterArgsBase /// Number of parameters required by custom command / transaction /// public int NumParams { get; set; } + + /// + /// RESP command info + /// + public RespCommandsInfo CommandInfo { get; set; } } @@ -171,7 +176,13 @@ public RegisterRawStringFunctionProvider(CustomRawStringFunctions instance, Regi public override void Register(CustomCommandManager customCommandManager) { - customCommandManager.Register(this.RegisterArgs.Name, this.RegisterArgs.NumParams, this.RegisterArgs.CommandType, this.Instance, this.RegisterArgs.ExpirationTicks); + customCommandManager.Register( + this.RegisterArgs.Name, + this.RegisterArgs.NumParams, + this.RegisterArgs.CommandType, + this.Instance, + this.RegisterArgs.CommandInfo, + this.RegisterArgs.ExpirationTicks); } } @@ -186,7 +197,7 @@ public RegisterCustomObjectFactoryProvider(CustomObjectFactory instance, Registe public override void Register(CustomCommandManager customCommandManager) { - customCommandManager.Register(this.RegisterArgs.Name, this.RegisterArgs.NumParams, this.RegisterArgs.CommandType, this.Instance); + customCommandManager.Register(this.RegisterArgs.Name, this.RegisterArgs.NumParams, this.RegisterArgs.CommandType, this.Instance, this.RegisterArgs.CommandInfo); } } diff --git a/libs/server/Garnet.server.csproj b/libs/server/Garnet.server.csproj index 5aa72ef9f5..2950ad32b4 100644 --- a/libs/server/Garnet.server.csproj +++ b/libs/server/Garnet.server.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/libs/server/Resp/AdminCommands.cs b/libs/server/Resp/AdminCommands.cs index e6bb1c0814..d6b575a1bc 100644 --- a/libs/server/Resp/AdminCommands.cs +++ b/libs/server/Resp/AdminCommands.cs @@ -248,36 +248,6 @@ private bool ProcessAdminCommands(RespCommand command, ReadOnlySpan< { return ProcessInfoCommand(count); } - else if (command == RespCommand.COMMAND) - { - if (count != 0) - { - if (!DrainCommands(bufSpan, count)) - return false; - errorFlag = true; - errorCmd = "command"; - } - else - { - // TODO: include the built-in commands - string resultStr = ""; - int cnt = 0; - for (int i = 0; i < storeWrapper.customCommandManager.CommandId; i++) - { - var cmd = storeWrapper.customCommandManager.commandMap[i]; - if (cmd != null) - { - cnt++; - resultStr += $"*6\r\n${cmd.nameStr.Length}\r\n{cmd.nameStr}\r\n:{1 + cmd.NumKeys + cmd.NumParams}\r\n*1\r\n+fast\r\n:1\r\n:1\r\n:1\r\n"; - } - } - - while (!RespWriteUtils.WriteAsciiDirect($"*{cnt}\r\n", ref dcurr, dend)) - SendAndReset(); - while (!RespWriteUtils.WriteAsciiDirect(resultStr, ref dcurr, dend)) - SendAndReset(); - } - } else if (command == RespCommand.PING) { if (count == 0) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 2643d3f660..3829ddcdb2 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text; using Garnet.common; using Tsavorite.core; @@ -95,7 +96,8 @@ bool NetworkGET_SG(byte* ptr, ref TGarnetApi storageApi) // Store index in context, since completions are not in order ctx = firstPending == -1 ? 0 : c - firstPending; - var status = storageApi.GET_WithPending(ref Unsafe.AsRef(keyPtr), ref input, ref o, ctx, out bool isPending); + var status = storageApi.GET_WithPending(ref Unsafe.AsRef(keyPtr), ref input, ref o, ctx, + out bool isPending); if (isPending) { @@ -195,7 +197,8 @@ bool ParseGETAndKey(ref byte* keyPtr, ref int ksize, ref byte* ptr) return true; } - static void SetResult(int c, ref int firstPending, ref (GarnetStatus, SpanByteAndMemory)[] outputArr, GarnetStatus status, SpanByteAndMemory output) + static void SetResult(int c, ref int firstPending, ref (GarnetStatus, SpanByteAndMemory)[] outputArr, + GarnetStatus status, SpanByteAndMemory output) { const int initialBatchSize = 8; // number of items in initial batch if (firstPending == -1) @@ -215,6 +218,7 @@ static void SetResult(int c, ref int firstPending, ref (GarnetStatus, SpanByteAn Array.Copy(outputArr, outputArr2, outputArr.Length); outputArr = outputArr2; } + outputArr[c - firstPending] = (status, output); } @@ -277,7 +281,8 @@ private bool NetworkSetRange(byte* ptr, ref TGarnetApi storageApi) byte* offsetPtr = null; int offsetSize = 0; - if (!RespReadUtils.ReadPtrWithLengthHeader(ref offsetPtr, ref offsetSize, ref ptr, recvBufferPtr + bytesRead)) + if (!RespReadUtils.ReadPtrWithLengthHeader(ref offsetPtr, ref offsetSize, ref ptr, + recvBufferPtr + bytesRead)) return false; byte* valPtr = null; @@ -393,7 +398,10 @@ private bool NetworkSETEX(byte* ptr, bool highPrecision, ref TGarnet valPtr -= sizeof(int) + sizeof(long); *(int*)keyPtr = ksize; *(int*)valPtr = vsize + sizeof(long); // expiry info - SpanByte.Reinterpret(valPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + (highPrecision ? TimeSpan.FromMilliseconds(expiry).Ticks : TimeSpan.FromSeconds(expiry).Ticks); + SpanByte.Reinterpret(valPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + + (highPrecision + ? TimeSpan.FromMilliseconds(expiry).Ticks + : TimeSpan.FromSeconds(expiry).Ticks); var status = storageApi.SET(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(valPtr)); while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) @@ -404,12 +412,19 @@ private bool NetworkSETEX(byte* ptr, bool highPrecision, ref TGarnet enum ExpirationOption : byte { - None, EX, PX, EXAT, PXAT, KEEPTTL + None, + EX, + PX, + EXAT, + PXAT, + KEEPTTL } enum ExistOptions : byte { - None, NX, XX + None, + NX, + XX } /// @@ -464,6 +479,7 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto error = true; continue; } + expOption = ExpirationOption.EX; if (expiry <= 0) { @@ -487,6 +503,7 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto error = true; continue; } + expOption = ExpirationOption.PX; if (expiry <= 0) { @@ -495,7 +512,8 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto continue; } } - else if (*(long*)ptr == 5784105485020772132 && *(int*)(ptr + 8) == 223106132 && *(ptr + 12) == 10) // [KEEPTTL] + else if (*(long*)ptr == 5784105485020772132 && *(int*)(ptr + 8) == 223106132 && + *(ptr + 12) == 10) // [KEEPTTL] { ptr += 13; count--; @@ -505,6 +523,7 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto error = true; continue; } + expOption = ExpirationOption.KEEPTTL; } else if (*(long*)ptr == 724332207275848228) // [NX] @@ -517,6 +536,7 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto error = true; continue; } + existOptions = ExistOptions.NX; } else if (*(long*)ptr == 724332250225521188) // [XX] @@ -529,6 +549,7 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto error = true; continue; } + existOptions = ExistOptions.XX; } else if (*(long*)ptr == 960468791950390052 && *(ptr + 8) == 10) // [GET] @@ -575,27 +596,37 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto switch (existOptions) { case ExistOptions.None: - return getValue ? - NetworkSET_Conditional(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, getValue, false, ref storageApi) : - NetworkSET_EX(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, false, ref storageApi); // Can perform a blind update + return getValue + ? NetworkSET_Conditional(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, getValue, + false, ref storageApi) + : NetworkSET_EX(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, false, + ref storageApi); // Can perform a blind update case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, ptr, expiry, keyPtr, valPtr, vsize, getValue, false, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETEXXX, ptr, expiry, keyPtr, valPtr, vsize, + getValue, false, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, ptr, expiry, keyPtr, valPtr, vsize, getValue, false, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETEXNX, ptr, expiry, keyPtr, valPtr, vsize, + getValue, false, ref storageApi); } + break; case ExpirationOption.PX: switch (existOptions) { case ExistOptions.None: - return getValue ? - NetworkSET_Conditional(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, getValue, true, ref storageApi) : - NetworkSET_EX(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, true, ref storageApi); // Can perform a blind update + return getValue + ? NetworkSET_Conditional(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, getValue, + true, ref storageApi) + : NetworkSET_EX(RespCommand.SET, ptr, expiry, keyPtr, valPtr, vsize, true, + ref storageApi); // Can perform a blind update case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, ptr, expiry, keyPtr, valPtr, vsize, getValue, true, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETEXXX, ptr, expiry, keyPtr, valPtr, vsize, + getValue, true, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, ptr, expiry, keyPtr, valPtr, vsize, getValue, true, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETEXNX, ptr, expiry, keyPtr, valPtr, vsize, + getValue, true, ref storageApi); } + break; case ExpirationOption.KEEPTTL: @@ -604,12 +635,16 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto { case ExistOptions.None: // We can never perform a blind update due to KEEPTTL - return NetworkSET_Conditional(RespCommand.SETKEEPTTL, ptr, expiry, keyPtr, valPtr, vsize, getValue, false, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETKEEPTTL, ptr, expiry, keyPtr, valPtr, vsize, + getValue, false, ref storageApi); case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, ptr, expiry, keyPtr, valPtr, vsize, getValue, false, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, ptr, expiry, keyPtr, valPtr, vsize, + getValue, false, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, ptr, expiry, keyPtr, valPtr, vsize, getValue, false, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETEXNX, ptr, expiry, keyPtr, valPtr, vsize, + getValue, false, ref storageApi); } + break; } @@ -618,7 +653,8 @@ private bool NetworkSETEXNX(int count, byte* ptr, ref TGarnetApi sto return true; } - private bool NetworkSET_EX(RespCommand cmd, byte* ptr, int expiry, byte* keyPtr, byte* valPtr, int vsize, bool highPrecision, ref TGarnetApi storageApi) + private bool NetworkSET_EX(RespCommand cmd, byte* ptr, int expiry, byte* keyPtr, byte* valPtr, + int vsize, bool highPrecision, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { Debug.Assert(cmd == RespCommand.SET); @@ -632,7 +668,10 @@ private bool NetworkSET_EX(RespCommand cmd, byte* ptr, int expiry, b // Move payload forward to make space for metadata Buffer.MemoryCopy(valPtr + sizeof(int), valPtr + sizeof(int) + sizeof(long), vsize, vsize); *(int*)valPtr = vsize + sizeof(long); - SpanByte.Reinterpret(valPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + (highPrecision ? TimeSpan.FromMilliseconds(expiry).Ticks : TimeSpan.FromSeconds(expiry).Ticks); + SpanByte.Reinterpret(valPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + + (highPrecision + ? TimeSpan.FromMilliseconds(expiry).Ticks + : TimeSpan.FromSeconds(expiry).Ticks); } storageApi.SET(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(valPtr)); @@ -643,7 +682,8 @@ private bool NetworkSET_EX(RespCommand cmd, byte* ptr, int expiry, b return true; } - private bool NetworkSET_Conditional(RespCommand cmd, byte* ptr, int expiry, byte* keyPtr, byte* inputPtr, int isize, bool getValue, bool highPrecision, ref TGarnetApi storageApi) + private bool NetworkSET_Conditional(RespCommand cmd, byte* ptr, int expiry, byte* keyPtr, + byte* inputPtr, int isize, bool getValue, bool highPrecision, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { // Make space for RespCommand in input @@ -660,19 +700,24 @@ private bool NetworkSET_Conditional(RespCommand cmd, byte* ptr, int else { // Move payload forward to make space for metadata - Buffer.MemoryCopy(inputPtr + sizeof(int) + RespInputHeader.Size, inputPtr + sizeof(int) + sizeof(long) + RespInputHeader.Size, isize, isize); + Buffer.MemoryCopy(inputPtr + sizeof(int) + RespInputHeader.Size, + inputPtr + sizeof(int) + sizeof(long) + RespInputHeader.Size, isize, isize); *(int*)inputPtr = sizeof(long) + RespInputHeader.Size + isize; ((RespInputHeader*)(inputPtr + sizeof(int) + sizeof(long)))->cmd = cmd; ((RespInputHeader*)(inputPtr + sizeof(int) + sizeof(long)))->flags = 0; if (getValue) ((RespInputHeader*)(inputPtr + sizeof(int) + sizeof(long)))->SetSetGetFlag(); - SpanByte.Reinterpret(inputPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + (highPrecision ? TimeSpan.FromMilliseconds(expiry).Ticks : TimeSpan.FromSeconds(expiry).Ticks); + SpanByte.Reinterpret(inputPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + + (highPrecision + ? TimeSpan.FromMilliseconds(expiry).Ticks + : TimeSpan.FromSeconds(expiry).Ticks); } if (getValue) { var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(inputPtr), ref o); + var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), + ref Unsafe.AsRef(inputPtr), ref o); // Status tells us whether an old image was found during RMW or not if (status == GarnetStatus.NOTFOUND) @@ -691,7 +736,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, byte* ptr, int } else { - var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(inputPtr)); + var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), + ref Unsafe.AsRef(inputPtr)); bool ok = status != GarnetStatus.NOTFOUND; @@ -722,7 +768,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, byte* ptr, int private bool NetworkIncrement(byte* ptr, RespCommand cmd, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - Debug.Assert(cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY || cmd == RespCommand.INCR || cmd == RespCommand.DECR); + Debug.Assert(cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY || cmd == RespCommand.INCR || + cmd == RespCommand.DECR); // Parse key argument byte* keyPtr = null; @@ -777,7 +824,9 @@ private bool NetworkIncrement(byte* ptr, RespCommand cmd, ref TGarne var output = ArgSlice.FromPinnedSpan(outputBuffer); var status = storageApi.Increment(key, input, ref output); - var errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1 ? (OperationError)output.Span[0] : OperationError.SUCCESS; + var errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1 + ? (OperationError)output.Span[0] + : OperationError.SUCCESS; switch (errorFlag) { @@ -786,12 +835,14 @@ private bool NetworkIncrement(byte* ptr, RespCommand cmd, ref TGarne SendAndReset(); break; case OperationError.INVALID_TYPE: - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend)) + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, + dend)) SendAndReset(); break; default: throw new GarnetException($"Invalid OperationError {errorFlag}"); } + return true; } @@ -823,7 +874,8 @@ private bool NetworkAppend(byte* ptr, ref TGarnetApi storageApi) Span outputBuffer = stackalloc byte[NumUtils.MaximumFormatInt64Length]; var output = SpanByteAndMemory.FromPinnedSpan(outputBuffer); - var status = storageApi.APPEND(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(valPtr), ref output); + var status = storageApi.APPEND(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(valPtr), + ref output); while (!RespWriteUtils.WriteIntegerFromBytes(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) SendAndReset(); @@ -899,7 +951,7 @@ private bool NetworkREADWRITE() /// /// private bool NetworkSTRLEN(byte* ptr, ref TGarnetApi storageApi) - where TGarnetApi : IGarnetApi + where TGarnetApi : IGarnetApi { byte* keyPtr = null; int ksize = 0; @@ -931,5 +983,110 @@ private bool NetworkSTRLEN(byte* ptr, ref TGarnetApi storageApi) return true; } + private bool NetworkCOMMAND(int count) + { + var subCommand = ReadOnlySpan.Empty; + ReadOnlySpan bufSpan = new(recvBufferPtr, bytesRead); + + if (count > 0) + { + subCommand = GetCommand(bufSpan, out var success); + if (!success) + return false; + } + + // Handle COMMAND COUNT + if (count > 0 && (subCommand.SequenceEqual(CmdStrings.COUNT) || subCommand.SequenceEqual(CmdStrings.count))) + { + if (!RespCommandsInfo.TryGetRespCommandsInfoCount(out var respCommandCount, true, logger)) + { + respCommandCount = 0; + } + + var commandCount = storeWrapper.customCommandManager.CustomCommandsInfoCount + respCommandCount; + + while (!RespWriteUtils.WriteInteger(commandCount, ref dcurr, dend)) + SendAndReset(); + } + // Handle COMMAND and COMMAND INFO + else if (count == 0 || subCommand.SequenceEqual(CmdStrings.INFO) || subCommand.SequenceEqual(CmdStrings.info)) + { + // Handle COMMAND and COMMAND INFO without command names - return all commands + if (count < 2) + { + var resultSb = new StringBuilder(); + var cmdCount = 0; + + foreach (var customCmd in storeWrapper.customCommandManager.CustomCommandsInfo) + { + cmdCount++; + resultSb.Append(customCmd.RespFormat); + } + + if (RespCommandsInfo.TryGetRespCommandsInfo(out var respCommandsInfo, true, logger)) + { + foreach (var cmd in respCommandsInfo.Values) + { + cmdCount++; + resultSb.Append(cmd.RespFormat); + } + } + + while (!RespWriteUtils.WriteArrayLength(cmdCount, ref dcurr, dend)) + SendAndReset(); + while (!RespWriteUtils.WriteAsciiDirect(resultSb.ToString(), ref dcurr, dend)) + SendAndReset(); + } + // Handle COMMAND INFO with command names - return all commands specified + else + { + RespWriteUtils.WriteArrayLength(count - 1, ref dcurr, dend); + + for (var i = 0; i < count - 1; i++) + { + var cmdNameSpan = GetCommand(bufSpan, out var success); + if (!success) + return false; + + var cmdName = Encoding.ASCII.GetString(cmdNameSpan); + + if (RespCommandsInfo.TryGetRespCommandInfo(cmdName, out var cmdInfo, logger) || + storeWrapper.customCommandManager.TryGetCustomCommandInfo(cmdName, out cmdInfo)) + { + while (!RespWriteUtils.WriteAsciiDirect(cmdInfo.RespFormat, ref dcurr, dend)) + SendAndReset(); + } + else + { + while (!RespWriteUtils.WriteNull(ref dcurr, dend)) + SendAndReset(); + } + } + } + } + // Placeholder for handling DOCS sub-command - returning Nil in the meantime. + else if (count > 0 && (subCommand.SequenceEqual(CmdStrings.DOCS) || subCommand.SequenceEqual(CmdStrings.docs))) + { + if (!DrainCommands(bufSpan, count - 1)) + return false; + + while (!RespWriteUtils.WriteEmptyArray(ref dcurr, dend)) + SendAndReset(); + } + // Unsupported COMMAND subcommand + else + { + if (!DrainCommands(bufSpan, count - 1)) + return false; + + var subCmdName = Encoding.ASCII.GetString(subCommand); + var errorMsg = string.Format(CmdStrings.GenericErrUnknownSubCommand, subCmdName, RespCommand.COMMAND); + + while (!RespWriteUtils.WriteError(errorMsg, ref dcurr, dend)) + SendAndReset(); + } + + return true; + } } } \ No newline at end of file diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index 9b3fd23c8b..d81514fd35 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -42,6 +42,9 @@ public static ReadOnlySpan GetConfig(ReadOnlySpan key) public static ReadOnlySpan AUTH => "AUTH"u8; public static ReadOnlySpan auth => "auth"u8; public static ReadOnlySpan INFO => "INFO"u8; + public static ReadOnlySpan info => "info"u8; + public static ReadOnlySpan DOCS => "DOCS"u8; + public static ReadOnlySpan docs => "docs"u8; public static ReadOnlySpan COMMAND => "COMMAND"u8; public static ReadOnlySpan LATENCY => "LATENCY"u8; public static ReadOnlySpan CLUSTER => "CLUSTER"u8; @@ -132,6 +135,7 @@ public static ReadOnlySpan GetConfig(ReadOnlySpan key) /// public const string GenericErrWrongNumArgs = "ERR wrong number of arguments for '{0}' command"; public const string GenericErrUnknownOption = "ERR Unknown option or number of arguments for CONFIG SET - '{0}'"; + public const string GenericErrUnknownSubCommand = "ERR unknown subcommand '{0}'. Try {1} HELP"; /// /// Object types diff --git a/libs/server/Resp/IRespSerializable.cs b/libs/server/Resp/IRespSerializable.cs new file mode 100644 index 0000000000..8bc3b48dbd --- /dev/null +++ b/libs/server/Resp/IRespSerializable.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Garnet.server +{ + /// + /// Interface to define classes that are serializable to RESP format + /// + public interface IRespSerializable + { + /// + /// Serializes the current object to RESP format + /// + /// Serialized value in RESP format + string ToRespFormat(); + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespCommand.cs b/libs/server/Resp/RespCommand.cs index 7a57c267c4..9b2affb0a5 100644 --- a/libs/server/Resp/RespCommand.cs +++ b/libs/server/Resp/RespCommand.cs @@ -79,7 +79,6 @@ public enum RespCommand : byte PSUBSCRIBE = 0x9, UNSUBSCRIBE = 0xA, PUNSUBSCRIBE = 0xB, - NOAUTH = 0xC, ASKING = 0xD, MIGRATE = 0xE, SELECT = 0xF, diff --git a/libs/server/Resp/RespCommandInfoFlags.cs b/libs/server/Resp/RespCommandInfoFlags.cs new file mode 100644 index 0000000000..08b0fd1ba1 --- /dev/null +++ b/libs/server/Resp/RespCommandInfoFlags.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.ComponentModel; + +namespace Garnet.server +{ + /// + /// RESP command flags + /// + [Flags] + public enum RespCommandFlags + { + None = 0, + [Description("admin")] + Admin = 1, + [Description("asking")] + Asking = 1 << 1, + [Description("blocking")] + Blocking = 1 << 2, + [Description("denyoom")] + DenyOom = 1 << 3, + [Description("fast")] + Fast = 1 << 4, + [Description("loading")] + Loading = 1 << 5, + [Description("movablekeys")] + MovableKeys = 1 << 6, + [Description("no_auth")] + NoAuth = 1 << 7, + [Description("no_async_loading")] + NoAsyncLoading = 1 << 8, + [Description("no_mandatory_keys")] + NoMandatoryKeys = 1 << 9, + [Description("no_multi")] + NoMulti = 1 << 10, + [Description("noscript")] + NoScript = 1 << 11, + [Description("pubsub")] + PubSub = 1 << 12, + [Description("random")] + Random = 1 << 13, + [Description("readonly")] + ReadOnly = 1 << 14, + [Description("sort_for_script")] + SortForScript = 1 << 15, + [Description("skip_monitor")] + SkipMonitor = 1 << 16, + [Description("skip_slowlog")] + SkipSlowLog = 1 << 17, + [Description("stale")] + Stale = 1 << 18, + [Description("write")] + Write = 1 << 19, + [Description("allow_busy")] + AllowBusy = 1 << 20, + } + + /// + /// RESP ACL categories + /// + [Flags] + public enum RespAclCategories + { + None = 0, + [Description("admin")] + Admin = 1, + [Description("bitmap")] + Bitmap = 1 << 1, + [Description("blocking")] + Blocking = 1 << 2, + [Description("connection")] + Connection = 1 << 3, + [Description("dangerous")] + Dangerous = 1 << 4, + [Description("geo")] + Geo = 1 << 5, + [Description("hash")] + Hash = 1 << 6, + [Description("hyperloglog")] + HyperLogLog = 1 << 7, + [Description("fast")] + Fast = 1 << 8, + [Description("keyspace")] + KeySpace = 1 << 9, + [Description("list")] + List = 1 << 10, + [Description("pubsub")] + PubSub = 1 << 11, + [Description("read")] + Read = 1 << 12, + [Description("scripting")] + Scripting = 1 << 13, + [Description("set")] + Set = 1 << 14, + [Description("sortedset")] + SortedSet = 1 << 15, + [Description("slow")] + Slow = 1 << 16, + [Description("stream")] + Stream = 1 << 17, + [Description("string")] + String = 1 << 18, + [Description("transaction")] + Transaction = 1 << 19, + [Description("write")] + Write = 1 << 20, + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespCommandInfoParser.cs b/libs/server/Resp/RespCommandInfoParser.cs new file mode 100644 index 0000000000..b3009ec4a8 --- /dev/null +++ b/libs/server/Resp/RespCommandInfoParser.cs @@ -0,0 +1,514 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Garnet.common; + +namespace Garnet.server +{ + /// + /// Logic for parsing command info from RESP format + /// + public class RespCommandInfoParser + { + /// + /// Tries to parse a RespCommandInfo object from RESP format + /// + /// Pointer to current RESP chunk to read + /// Pointer to end of RESP chunk to read + /// Mapping between command name and Garnet RespCommand and ArrayCommand values + /// Parsed RespCommandsInfo object + /// Name of parent command, null if none + /// True if parsing successful + public static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, IReadOnlyDictionary supportedCommands, out RespCommandsInfo commandInfo, string parentCommand = null) + { + commandInfo = default; + + // Command info is null + if (new ReadOnlySpan(ptr, 5).SequenceEqual("$-1\r\n"u8)) return true; + + // Verify command info array length + if (!RespReadUtils.ReadArrayLength(out var infoElemCount, ref ptr, end) + || infoElemCount != 10) return false; + + // 1) Name + if (!RespReadUtils.ReadStringWithLengthHeader(out var name, ref ptr, end)) return false; + + // 2) Arity + if (!RespReadUtils.ReadIntegerAsString(out var strArity, ref ptr, end) + || !int.TryParse(strArity, out var arity)) return false; + + // 3) Flags + var flags = RespCommandFlags.None; + if (!RespReadUtils.ReadArrayLength(out var flagCount, ref ptr, end)) return false; + for (var flagIdx = 0; flagIdx < flagCount; flagIdx++) + { + if (!RespReadUtils.ReadSimpleString(out var strFlag, ref ptr, end) + || !EnumUtils.TryParseEnumFromDescription(strFlag, out var flag)) + return false; + flags |= flag; + } + + // 4) First key + if (!RespReadUtils.ReadIntegerAsString(out var strFirstKey, ref ptr, end) + || !int.TryParse(strFirstKey, out var firstKey)) return false; + + // 5) Last key + if (!RespReadUtils.ReadIntegerAsString(out var strLastKey, ref ptr, end) + || !int.TryParse(strLastKey, out var lastKey)) return false; + + // 6) Step + if (!RespReadUtils.ReadIntegerAsString(out var strStep, ref ptr, end) + || !int.TryParse(strStep, out var step)) return false; + + // 7) ACL categories + var aclCategories = RespAclCategories.None; + if (!RespReadUtils.ReadArrayLength(out var aclCatCount, ref ptr, end)) return false; + for (var aclCatIdx = 0; aclCatIdx < aclCatCount; aclCatIdx++) + { + if (!RespReadUtils.ReadSimpleString(out var strAclCat, ref ptr, end) + || !EnumUtils.TryParseEnumFromDescription(strAclCat.TrimStart('@'), out var aclCat)) + return false; + aclCategories |= aclCat; + } + + // 8) Tips + if (!RespReadUtils.ReadStringArrayWithLengthHeader(out var tips, ref ptr, end)) return false; + + // 9) Key specifications + if (!RespReadUtils.ReadArrayLength(out var ksCount, ref ptr, end)) return false; + var keySpecifications = new RespCommandKeySpecification[ksCount]; + for (var ksIdx = 0; ksIdx < ksCount; ksIdx++) + { + if (!RespKeySpecificationParser.TryReadFromResp(ref ptr, end, out var keySpec)) return false; + keySpecifications[ksIdx] = keySpec; + } + + // 10) SubCommands + if (!RespReadUtils.ReadArrayLength(out var scCount, ref ptr, end)) return false; + var subCommands = new List(); + for (var scIdx = 0; scIdx < scCount; scIdx++) + { + if (!TryReadFromResp(ref ptr, end, supportedCommands, out commandInfo, name)) + return false; + + subCommands.Add(commandInfo); + } + + commandInfo = new RespCommandsInfo() + { + Command = supportedCommands[parentCommand ?? name].Item1, + ArrayCommand = supportedCommands[parentCommand ?? name].Item2, + Name = name.ToUpper(), + Arity = arity, + Flags = flags, + FirstKey = firstKey, + LastKey = lastKey, + Step = step, + AclCategories = aclCategories, + Tips = tips.Length == 0 ? null : tips, + KeySpecifications = keySpecifications.Length == 0 ? null : keySpecifications, + SubCommands = subCommands.Count == 0 ? null : subCommands.OrderBy(sc => sc.Name).ToArray() + }; + + return true; + } + } + + /// + /// Logic for parsing key specification from RESP format + /// + internal class RespKeySpecificationParser + { + /// + /// Tries to parse RespCommandKeySpecification from RESP format + /// + /// Pointer to current RESP chunk to read + /// Pointer to end of RESP chunk to read + /// Parsed RespCommandKeySpecification object + /// True if parsing successful + internal static unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out RespCommandKeySpecification keySpec) + { + keySpec = default; + + string notes = null; + var flags = KeySpecificationFlags.None; + KeySpecMethodBase beginSearch = null; + KeySpecMethodBase findKeys = null; + + if (!RespReadUtils.ReadArrayLength(out var elemCount, ref ptr, end)) return false; + + for (var elemIdx = 0; elemIdx < elemCount; elemIdx += 2) + { + if (!RespReadUtils.ReadStringWithLengthHeader(out var ksKey, ref ptr, end)) return false; + + if (string.Equals(ksKey, "notes", StringComparison.Ordinal)) + { + if (!RespReadUtils.ReadStringWithLengthHeader(out notes, ref ptr, end)) return false; + } + else if (string.Equals(ksKey, "flags", StringComparison.Ordinal)) + { + if (!RespReadUtils.ReadArrayLength(out var flagsCount, ref ptr, end)) return false; + for (var flagIdx = 0; flagIdx < flagsCount; flagIdx++) + { + if (!RespReadUtils.ReadSimpleString(out var strFlag, ref ptr, end) + || !EnumUtils.TryParseEnumFromDescription(strFlag, out var flag)) + return false; + flags |= flag; + } + } + else if (string.Equals(ksKey, "begin_search", StringComparison.Ordinal)) + { + if (!RespKeySpecificationTypesParser.TryReadFromResp(ksKey, ref ptr, end, out beginSearch)) return false; + } + else if (string.Equals(ksKey, "find_keys", StringComparison.Ordinal)) + { + if (!RespKeySpecificationTypesParser.TryReadFromResp(ksKey, ref ptr, end, out findKeys)) return false; + } + else + { + return false; + } + } + + keySpec = new RespCommandKeySpecification() + { + Notes = notes, + Flags = flags, + BeginSearch = beginSearch, + FindKeys = findKeys + }; + + return true; + } + } + + /// + /// Logic for parsing BeginSearch / FindKeys key specification from RESP format + /// + internal class RespKeySpecificationTypesParser + { + /// + /// Tries to parse KeySpecMethodBase from RESP format + /// + /// Type of key specification ("begin_search" / "find_keys") + /// Pointer to current RESP chunk to read + /// Pointer to end of RESP chunk to read + /// Parsed KeySpecMethodBase object + /// True if parsing successful + public static unsafe bool TryReadFromResp(string keySpecKey, ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!TryReadKeySpecHeader(ref ptr, end, out var keySpecType)) return false; + + IKeySpecParser parser; + if (string.Equals(keySpecKey, "begin_search", StringComparison.Ordinal)) + { + if (string.Equals(keySpecType, "index", StringComparison.Ordinal)) + parser = BeginSearchIndexParser.Instance; + else if (string.Equals(keySpecType, "keyword", StringComparison.Ordinal)) + parser = BeginSearchKeywordParser.Instance; + else if (string.Equals(keySpecType, "unknown", StringComparison.Ordinal)) + parser = BeginSearchUnknownParser.Instance; + else return false; + } + else if (string.Equals(keySpecKey, "find_keys", StringComparison.Ordinal)) + { + if (string.Equals(keySpecType, "range", StringComparison.Ordinal)) + parser = FindKeysRangeParser.Instance; + else if (string.Equals(keySpecType, "keynum", StringComparison.Ordinal)) + parser = FindKeysKeyNumParser.Instance; + else if (string.Equals(keySpecType, "unknown", StringComparison.Ordinal)) + parser = FindKeysUnknownParser.Instance; + else return false; + } + else return false; + + if (!parser.TryReadFromResp(ref ptr, end, out keySpecMethod)) return false; + + return true; + } + + /// + /// Tries to parse key spec header from RESP format + /// + /// Pointer to current RESP chunk to read + /// Pointer to end of RESP chunk to read + /// Parsed key spec type + /// True if parsing successful + private static unsafe bool TryReadKeySpecHeader(ref byte* ptr, byte* end, out string keySpecType) + { + keySpecType = default; + + if (!RespReadUtils.ReadArrayLength(out var ksTypeElemCount, ref ptr, end) + || ksTypeElemCount != 4 + || !RespReadUtils.ReadStringWithLengthHeader(out var ksTypeStr, ref ptr, end) + || !string.Equals(ksTypeStr, "type", StringComparison.Ordinal) + || !RespReadUtils.ReadStringWithLengthHeader(out var ksType, ref ptr, end) + || !RespReadUtils.ReadStringWithLengthHeader(out var ksSpecStr, ref ptr, end) + || !string.Equals(ksSpecStr, "spec", StringComparison.Ordinal)) return false; + + keySpecType = ksType; + return true; + } + + /// + /// Interface for classes implementing parsing of KeySpecMethodBase objects + /// + internal interface IKeySpecParser + { + /// + /// Tries to parse KeySpecMethodBase from RESP format + /// + /// Pointer to current RESP chunk to read + /// Pointer to end of RESP chunk to read + /// Parsed KeySpecMethodBase object + /// True if parsing successful + unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod); + } + + /// + /// Parser for the BeginSearchIndex key specification method + /// + internal sealed class BeginSearchIndexParser : IKeySpecParser + { + private static BeginSearchIndexParser ParserInstance; + + /// + /// Disallow default constructor (singleton) + /// + private BeginSearchIndexParser() { } + + /// + /// Returns the singleton instance of . + /// + public static BeginSearchIndexParser Instance + { + get { return ParserInstance ??= new BeginSearchIndexParser(); } + } + + /// + public unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!RespReadUtils.ReadArrayLength(out var ksSpecElemCount, ref ptr, end) + || ksSpecElemCount != 2 + || !RespReadUtils.ReadStringWithLengthHeader(out var ksArgKey, ref ptr, end) + || !string.Equals(ksArgKey, "index", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strIndex, ref ptr, end) + || !int.TryParse(strIndex, out var index)) return false; + + keySpecMethod = new BeginSearchIndex(index); + + return true; + } + } + + /// + /// Parser for the BeginSearchKeyword key specification method + /// + + internal sealed class BeginSearchKeywordParser : IKeySpecParser + { + private static BeginSearchKeywordParser ParserInstance; + + /// + /// Disallow default constructor (singleton) + /// + private BeginSearchKeywordParser() { } + + /// + /// Returns the singleton instance of . + /// + public static BeginSearchKeywordParser Instance + { + get { return ParserInstance ??= new BeginSearchKeywordParser(); } + } + + /// + public unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!RespReadUtils.ReadArrayLength(out var specElemCount, ref ptr, end) + || specElemCount != 4 + || !RespReadUtils.ReadStringWithLengthHeader(out var argKey, ref ptr, end) + || !string.Equals(argKey, "keyword", StringComparison.Ordinal) + || !RespReadUtils.ReadStringWithLengthHeader(out var keyword, ref ptr, end) + || !RespReadUtils.ReadStringWithLengthHeader(out argKey, ref ptr, end) + || !string.Equals(argKey, "startfrom", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strStartFrom, ref ptr, end) + || !int.TryParse(strStartFrom, out var startFrom)) return false; + + keySpecMethod = new BeginSearchKeyword(keyword, startFrom); + + return true; + } + } + + /// + /// Parser for the BeginSearchUnknown key specification method + /// + internal sealed class BeginSearchUnknownParser : IKeySpecParser + { + private static BeginSearchUnknownParser ParserInstance; + + /// + /// Disallow default constructor (singleton) + /// + private BeginSearchUnknownParser() { } + + /// + /// Returns the singleton instance of . + /// + public static BeginSearchUnknownParser Instance + { + get { return ParserInstance ??= new BeginSearchUnknownParser(); } + } + + /// + public unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!RespReadUtils.ReadArrayLength(out var ksSpecElemCount, ref ptr, end) + || ksSpecElemCount == 0) return false; + + keySpecMethod = new BeginSearchUnknown(); + + return true; + } + } + + /// + /// Parser for the FindKeysRange key specification method + /// + internal sealed class FindKeysRangeParser : IKeySpecParser + { + private static FindKeysRangeParser ParserInstance; + + /// + /// Disallow default constructor (singleton) + /// + private FindKeysRangeParser() { } + + /// + /// Returns the singleton instance of . + /// + public static FindKeysRangeParser Instance + { + get { return ParserInstance ??= new FindKeysRangeParser(); } + } + + /// + public unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!RespReadUtils.ReadArrayLength(out var specElemCount, ref ptr, end) + || specElemCount != 6 + || !RespReadUtils.ReadStringWithLengthHeader(out var argKey, ref ptr, end) + || !string.Equals(argKey, "lastkey", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strLastKey, ref ptr, end) + || !int.TryParse(strLastKey, out var lastKey) + || !RespReadUtils.ReadStringWithLengthHeader(out argKey, ref ptr, end) + || !string.Equals(argKey, "keystep", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strKeyStep, ref ptr, end) + || !int.TryParse(strKeyStep, out var keyStep) + || !RespReadUtils.ReadStringWithLengthHeader(out argKey, ref ptr, end) + || !string.Equals(argKey, "limit", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strLimit, ref ptr, end) + || !int.TryParse(strLimit, out var limit)) return false; + + keySpecMethod = new FindKeysRange(lastKey, keyStep, limit); + + return true; + } + } + + /// + /// Parser for the FindKeysKeyNum key specification method + /// + internal sealed class FindKeysKeyNumParser : IKeySpecParser + { + private static FindKeysKeyNumParser ParserInstance; + + /// + /// Disallow default constructor (singleton) + /// + private FindKeysKeyNumParser() { } + + + /// + /// Returns the singleton instance of . + /// + public static FindKeysKeyNumParser Instance + { + get { return ParserInstance ??= new FindKeysKeyNumParser(); } + } + + /// + public unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!RespReadUtils.ReadArrayLength(out var specElemCount, ref ptr, end) + || specElemCount != 6 + || !RespReadUtils.ReadStringWithLengthHeader(out var argKey, ref ptr, end) + || !string.Equals(argKey, "keynumidx", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strKeyNumIdx, ref ptr, end) + || !int.TryParse(strKeyNumIdx, out var keyNumIdx) + || !RespReadUtils.ReadStringWithLengthHeader(out argKey, ref ptr, end) + || !string.Equals(argKey, "firstkey", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strFirstKey, ref ptr, end) + || !int.TryParse(strFirstKey, out var firstKey) + || !RespReadUtils.ReadStringWithLengthHeader(out argKey, ref ptr, end) + || !string.Equals(argKey, "keystep", StringComparison.Ordinal) + || !RespReadUtils.ReadIntegerAsString(out var strKeyStep, ref ptr, end) + || !int.TryParse(strKeyStep, out var keyStep)) return false; + + keySpecMethod = new FindKeysKeyNum(keyNumIdx, firstKey, keyStep); + + return true; + } + } + + /// + /// Parser for the FindKeysUnknown key specification method + /// + internal sealed class FindKeysUnknownParser : IKeySpecParser + { + private static FindKeysUnknownParser ParserInstance; + + /// + /// Disallow default constructor (singleton) + /// + private FindKeysUnknownParser() { } + + /// + /// Returns the singleton instance of . + /// + public static FindKeysUnknownParser Instance + { + get { return ParserInstance ??= new FindKeysUnknownParser(); } + } + + /// + public unsafe bool TryReadFromResp(ref byte* ptr, byte* end, out KeySpecMethodBase keySpecMethod) + { + keySpecMethod = default; + + if (!RespReadUtils.ReadArrayLength(out var ksSpecElemCount, ref ptr, end) + || ksSpecElemCount == 0) return false; + + keySpecMethod = new FindKeysUnknown(); + + return true; + } + } + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespCommandKeySpecification.cs b/libs/server/Resp/RespCommandKeySpecification.cs new file mode 100644 index 0000000000..f11292b8a9 --- /dev/null +++ b/libs/server/Resp/RespCommandKeySpecification.cs @@ -0,0 +1,576 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.ComponentModel; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Garnet.common; + +namespace Garnet.server +{ + /// + /// Represents a RESP command's key specification + /// A key specification describes a rule for extracting the names of one or more keys from the arguments of a given command + /// + public class RespCommandKeySpecification : IRespSerializable + { + /// + /// BeginSearch value of a specification informs the client of the extraction's beginning + /// + public KeySpecMethodBase BeginSearch { get; init; } + + /// + /// FindKeys value of a key specification tells the client how to continue the search for key names + /// + public KeySpecMethodBase FindKeys { get; init; } + + /// + /// Notes about non-obvious key specs considerations + /// + public string Notes { get; init; } + + /// + /// Flags that provide more details about the key + /// + public KeySpecificationFlags Flags + { + get => this.flags; + init + { + this.flags = value; + this.respFormatFlags = EnumUtils.GetEnumDescriptions(this.flags); + } + } + + /// + /// Returns the serialized representation of the current object in RESP format + /// This property returns a cached value, if exists (this value should never change after object initialization) + /// + [JsonIgnore] + public string RespFormat => respFormat ??= ToRespFormat(); + + private string respFormat; + private readonly KeySpecificationFlags flags; + private readonly string[] respFormatFlags; + + /// + /// Serializes the current object to RESP format + /// + /// Serialized value + public string ToRespFormat() + { + var sb = new StringBuilder(); + var elemCount = 0; + + if (this.Notes != null) + { + elemCount += 2; + sb.Append("$5\r\nnotes\r\n"); + sb.Append($"${this.Notes.Length}\r\n{this.Notes}\r\n"); + } + + if (this.Flags != KeySpecificationFlags.None) + { + elemCount += 2; + sb.Append("$5\r\nflags\r\n"); + sb.Append($"*{this.respFormatFlags.Length}\r\n"); + foreach (var flag in this.respFormatFlags) + sb.Append($"+{flag}\r\n"); + } + + if (this.BeginSearch != null) + { + elemCount += 2; + sb.Append(this.BeginSearch.RespFormat); + } + + if (this.FindKeys != null) + { + elemCount += 2; + sb.Append(this.FindKeys.RespFormat); + } + + return $"*{elemCount}\r\n{sb}"; + } + } + + /// + /// RESP key specification flags + /// + [Flags] + public enum KeySpecificationFlags : ushort + { + None = 0, + + // Access type flags + [Description("RW")] + RW = 1, + [Description("RO")] + RO = 1 << 1, + [Description("OW")] + OW = 1 << 2, + [Description("RM")] + RM = 1 << 3, + + // Logical operation flags + [Description("access")] + Access = 1 << 4, + [Description("update")] + Update = 1 << 5, + [Description("insert")] + Insert = 1 << 6, + [Description("delete")] + Delete = 1 << 7, + + // Miscellaneous flags + [Description("not_key")] + NotKey = 1 << 8, + [Description("incomplete")] + Incomplete = 1 << 9, + [Description("variable_flags")] + VariableFlags = 1 << 10, + } + + /// + /// Base class representing key specification methods + /// + public abstract class KeySpecMethodBase : IRespSerializable + { + /// + /// Name of the key specification method + /// + public abstract string MethodName { get; } + + /// + /// Type of the key specification method in RESP format + /// + public abstract string RespFormatType { get; } + + /// + /// Spec of the key specification method in RESP format + /// + public abstract string RespFormatSpec { get; } + + /// + /// Returns the serialized representation of the current object in RESP format + /// This property returns a cached value, if exists (this value should never change after object initialization) + /// + public string RespFormat => respFormat ??= ToRespFormat(); + + private string respFormat; + + /// + /// Serializes the current object to RESP format + /// + /// Serialized value + public string ToRespFormat() + { + var sb = new StringBuilder(); + sb.Append($"${this.MethodName.Length}\r\n{this.MethodName}\r\n"); + sb.Append("*4\r\n"); + sb.Append("$4\r\ntype\r\n"); + sb.Append($"{this.RespFormatType}\r\n"); + sb.Append("$4\r\nspec\r\n"); + sb.Append($"{this.RespFormatSpec}\r\n"); + return sb.ToString(); + } + } + + /// + /// Base class representing BeginSearch key specification method types + /// + public abstract class BeginSearchKeySpecMethodBase : KeySpecMethodBase + { + /// + /// Name of the key specification + /// + public sealed override string MethodName => "begin_search"; + } + + /// + /// Represents BeginSearch key specification method of type "index" + /// Indicates that input keys appear at a constant index + /// + public class BeginSearchIndex : BeginSearchKeySpecMethodBase + { + /// + /// The 0-based index from which the client should start extracting key names + /// + public int Index { get; init; } + + /// + [JsonIgnore] + public sealed override string RespFormatType => "$5\r\nindex"; + + /// + [JsonIgnore] + public sealed override string RespFormatSpec + { + get { return this.respFormatSpec ??= $"*2\r\n$5\r\nindex\r\n:{this.Index}"; } + } + + private string respFormatSpec; + + /// + public BeginSearchIndex() + { + } + + /// + public BeginSearchIndex(int index) : this() + { + this.Index = index; + } + } + + /// + /// Represents BeginSearch key specification method of type "keyword" + /// Indicates that a literal token precedes key name arguments + /// + public class BeginSearchKeyword : BeginSearchKeySpecMethodBase + { + /// + /// The keyword that marks the beginning of key name arguments + /// + public string Keyword { get; init; } + + /// + /// An index to the arguments array from which the client should begin searching + /// + public int StartFrom { get; init; } + + /// + [JsonIgnore] + public sealed override string RespFormatType => "$7\r\nkeyword"; + + /// + [JsonIgnore] + public sealed override string RespFormatSpec + { + get { return this.respFormatSpec ??= $"*4\r\n$7\r\nkeyword\r\n${this.Keyword?.Length ?? 0}\r\n{this.Keyword}\r\n$9\r\nstartfrom\r\n:{this.StartFrom}"; } + } + + private string respFormatSpec; + + /// + public BeginSearchKeyword() { } + + /// + public BeginSearchKeyword(string keyword, int startFrom) : this() + { + this.Keyword = keyword; + this.StartFrom = startFrom; + } + } + + /// + /// Represents BeginSearch key specification method of unknown type + /// + public class BeginSearchUnknown : BeginSearchKeySpecMethodBase + { + /// + [JsonIgnore] + public sealed override string RespFormatType => "$7\r\nunknown"; + + /// + [JsonIgnore] + public sealed override string RespFormatSpec + { + get { return this.respFormatSpec ??= $"*0"; } + } + + private string respFormatSpec; + } + + /// + /// Base class representing FindKeys key specification method types + /// + public abstract class FindKeysKeySpecMethodBase : KeySpecMethodBase + { + /// + /// Name of the key specification + /// + public sealed override string MethodName => "find_keys"; + } + + /// + /// Represents FindKeys key specification method of type "range" + /// Indicates that keys stop at a specific index or relative to the last argument + /// + public class FindKeysRange : FindKeysKeySpecMethodBase + { + /// + /// The index, relative to BeginSearch, of the last key argument + /// + public int LastKey { get; init; } + + /// + /// The number of arguments that should be skipped, after finding a key, to find the next one + /// + public int KeyStep { get; init; } + + /// + /// If LastKey is has the value of -1, Limit is used to stop the search by a factor + /// + public int Limit { get; init; } + + /// + [JsonIgnore] + public sealed override string RespFormatType => "$5\r\nrange"; + + /// + [JsonIgnore] + public sealed override string RespFormatSpec + { + get { return this.respFormatSpec ??= $"*6\r\n$7\r\nlastkey\r\n:{this.LastKey}\r\n$7\r\nkeystep\r\n:{this.KeyStep}\r\n$5\r\nlimit\r\n:{this.Limit}"; } + } + + private string respFormatSpec; + + /// + public FindKeysRange() { } + + /// + public FindKeysRange(int lastKey, int keyStep, int limit) : this() + { + this.LastKey = lastKey; + this.KeyStep = keyStep; + this.Limit = limit; + } + } + + /// + /// Represents FindKeys key specification method of type "keynum" + /// Indicates that an additional argument specifies the number of input keys + /// + public class FindKeysKeyNum : FindKeysKeySpecMethodBase + { + /// + /// The index, relative to BeginSearch, of the argument containing the number of keys + /// + public int KeyNumIdx { get; init; } + + /// + /// The index, relative to BeginSearch, of the first key + /// + public int FirstKey { get; init; } + + /// + /// The number of arguments that should be skipped, after finding a key, to find the next one + /// + public int KeyStep { get; init; } + + /// + [JsonIgnore] + public sealed override string RespFormatType => "$6\r\nkeynum"; + + /// + [JsonIgnore] + public sealed override string RespFormatSpec + { + get { return this.respFormatSpec ??= $"*6\r\n$9\r\nkeynumidx\r\n:{this.KeyNumIdx}\r\n$8\r\nfirstkey\r\n:{this.FirstKey}\r\n$7\r\nkeystep\r\n:{this.KeyStep}"; } + } + + private string respFormatSpec; + + /// + public FindKeysKeyNum() { } + + /// + public FindKeysKeyNum(int keyNumIdx, int firstKey, int keyStep) : this() + { + this.KeyNumIdx = keyNumIdx; + this.FirstKey = firstKey; + this.KeyStep = keyStep; + } + } + + /// + /// Represents FindKeys key specification method of unknown type + /// + public class FindKeysUnknown : FindKeysKeySpecMethodBase + { + /// + [JsonIgnore] + public sealed override string RespFormatType => "$7\r\nunknown"; + + /// + [JsonIgnore] + public sealed override string RespFormatSpec + { + get { return this.respFormatSpec ??= $"*0"; } + } + + private string respFormatSpec; + } + + /// + /// JSON converter for objects implementing KeySpecMethodBase + /// + public class KeySpecConverter : JsonConverter + { + /// + public override bool CanConvert(Type typeToConvert) => typeof(KeySpecMethodBase).IsAssignableFrom(typeToConvert); + + /// + public override KeySpecMethodBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (!typeof(KeySpecMethodBase).IsAssignableFrom(typeToConvert)) return null; + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + var propertyName = reader.GetString(); + if (propertyName != "TypeDiscriminator") + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var typeDiscriminator = reader.GetString(); + + var index = 0; + string keyword = null; + var startFrom = 0; + var lastKey = 0; + var keyStep = 0; + var limit = 0; + var keyNumIdx = 0; + var firstKey = 0; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return typeDiscriminator switch + { + nameof(BeginSearchIndex) => new BeginSearchIndex(index), + nameof(BeginSearchKeyword) => new BeginSearchKeyword(keyword, startFrom), + nameof(BeginSearchUnknown) => new BeginSearchUnknown(), + nameof(FindKeysRange) => new FindKeysRange(lastKey, keyStep, limit), + nameof(FindKeysKeyNum) => new FindKeysKeyNum(keyNumIdx, firstKey, keyStep), + nameof(FindKeysUnknown) => new FindKeysUnknown(), + _ => throw new JsonException() + }; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + propertyName = reader.GetString(); + reader.Read(); + + switch (typeDiscriminator) + { + case (nameof(BeginSearchIndex)): + switch (propertyName) + { + case nameof(BeginSearchIndex.Index): + index = reader.GetInt32(); + break; + } + + break; + case (nameof(BeginSearchKeyword)): + switch (propertyName) + { + case nameof(BeginSearchKeyword.Keyword): + keyword = reader.GetString(); + break; + case nameof(BeginSearchKeyword.StartFrom): + startFrom = reader.GetInt32(); + break; + } + + break; + case (nameof(FindKeysRange)): + switch (propertyName) + { + case nameof(FindKeysRange.LastKey): + lastKey = reader.GetInt32(); + break; + case nameof(FindKeysRange.KeyStep): + keyStep = reader.GetInt32(); + break; + case nameof(FindKeysRange.Limit): + limit = reader.GetInt32(); + break; + } + break; + case (nameof(FindKeysKeyNum)): + switch (propertyName) + { + case nameof(FindKeysKeyNum.KeyNumIdx): + keyNumIdx = reader.GetInt32(); + break; + case nameof(FindKeysKeyNum.FirstKey): + firstKey = reader.GetInt32(); + break; + case nameof(FindKeysKeyNum.KeyStep): + keyStep = reader.GetInt32(); + break; + } + break; + } + } + } + + throw new JsonException(); + } + + /// + public override void Write(Utf8JsonWriter writer, KeySpecMethodBase keySpecMethod, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + switch (keySpecMethod) + { + case BeginSearchIndex beginSearchIndex: + writer.WriteString("TypeDiscriminator", nameof(BeginSearchIndex)); + writer.WriteNumber(nameof(BeginSearchIndex.Index), beginSearchIndex.Index); + break; + case BeginSearchKeyword beginSearchKeyword: + writer.WriteString("TypeDiscriminator", nameof(BeginSearchKeyword)); + writer.WriteString(nameof(beginSearchKeyword.Keyword), beginSearchKeyword.Keyword); + writer.WriteNumber(nameof(beginSearchKeyword.StartFrom), beginSearchKeyword.StartFrom); + break; + case BeginSearchUnknown beginSearchUnknown: + writer.WriteString("TypeDiscriminator", nameof(BeginSearchUnknown)); + break; + case FindKeysRange findKeysRange: + writer.WriteString("TypeDiscriminator", nameof(FindKeysRange)); + writer.WriteNumber(nameof(FindKeysRange.LastKey), findKeysRange.LastKey); + writer.WriteNumber(nameof(FindKeysRange.KeyStep), findKeysRange.KeyStep); + writer.WriteNumber(nameof(FindKeysRange.Limit), findKeysRange.Limit); + break; + case FindKeysKeyNum findKeysKeyNum: + writer.WriteString("TypeDiscriminator", nameof(FindKeysKeyNum)); + writer.WriteNumber(nameof(FindKeysKeyNum.KeyNumIdx), findKeysKeyNum.KeyNumIdx); + writer.WriteNumber(nameof(FindKeysKeyNum.FirstKey), findKeysKeyNum.FirstKey); + writer.WriteNumber(nameof(FindKeysKeyNum.KeyStep), findKeysKeyNum.KeyStep); + break; + case FindKeysUnknown findKeysUnknown: + writer.WriteString("TypeDiscriminator", nameof(FindKeysUnknown)); + break; + default: + throw new JsonException(); + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespCommandsInfo.cs b/libs/server/Resp/RespCommandsInfo.cs index 29282ab641..9916a0ef69 100644 --- a/libs/server/Resp/RespCommandsInfo.cs +++ b/libs/server/Resp/RespCommandsInfo.cs @@ -3,268 +3,337 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json.Serialization; +using Garnet.common; +using Microsoft.Extensions.Logging; namespace Garnet.server { /// - /// Container for command information + /// Represents a RESP command's information /// - class RespCommandsInfo + public class RespCommandsInfo : IRespSerializable { - public readonly string nameStr; + /// + /// Garnet's RespCommand enum command representation + /// + public RespCommand Command { get; init; } + + /// + /// Garnet's sub-command enum value representation + /// + public byte? ArrayCommand { get; init; } /// - /// Number of arguments of the command. + /// The command's name /// - public readonly int arity; + public string Name { get; init; } /// - /// Name of the command + /// Determines if the command is Garnet internal-only (i.e. not exposed to clients) /// - public readonly byte[] name; - public readonly byte arrayCommand; + public bool IsInternal { get; init; } /// - /// Associated RESPCommand Id + /// The command's arity, i.e. the number of arguments a command expects + /// * A positive integer means a fixed number of arguments + /// * A negative integer means a minimal number of arguments /// - public readonly RespCommand command; - public readonly HashSet options; + public int Arity { get; init; } - public RespCommandsInfo(string name, RespCommand command, int arity, HashSet options) + /// + /// RESP command flags + /// + public RespCommandFlags Flags { - nameStr = name.ToUpperInvariant(); - this.name = System.Text.Encoding.ASCII.GetBytes(nameStr); - this.command = command; - this.arity = arity; - this.options = options; - this.arrayCommand = 255; + get => this.flags; + init + { + this.flags = value; + this.respFormatFlags = EnumUtils.GetEnumDescriptions(this.flags); + } } - public RespCommandsInfo(string name, RespCommand command, int arity, HashSet options, byte arrayCommand) : this(name, command, arity, options) + + /// + /// The position of the command's first key name argument + /// + public int FirstKey { get; init; } + + /// + /// The position of the command's last key name argument + /// + public int LastKey { get; init; } + + /// + /// The step, or increment, between the first key and the position of the next key + /// + public int Step { get; init; } + + /// + /// ACL categories to which the command belongs + /// + public RespAclCategories AclCategories { - this.arrayCommand = arrayCommand; + get => this.aclCategories; + init + { + this.aclCategories = value; + this.respFormatAclCategories = EnumUtils.GetEnumDescriptions(this.aclCategories); + } } /// - /// Check whether the option is for this command or not and returns RespCommandsOptionInfo + /// Helpful information about the command + /// + public string[] Tips { get; init; } + + /// + /// Methods for locating keys in the command's arguments /// - public bool MatchOptions(ReadOnlySpan command, out RespCommandsOptionInfo optionOutput) + public RespCommandKeySpecification[] KeySpecifications { get; init; } + + /// + /// All the command's sub-commands, if any + /// + public RespCommandsInfo[] SubCommands { get; init; } + + /// + /// Returns the serialized representation of the current object in RESP format + /// This property returns a cached value, if exists (this value should never change after object initialization) + /// + [JsonIgnore] + public string RespFormat => respFormat ??= ToRespFormat(); + + private const string RespCommandsEmbeddedFileName = @"RespCommandsInfo.json"; + + private string respFormat; + + private static bool IsInitialized = false; + private static readonly object IsInitializedLock = new(); + private static IReadOnlyDictionary AllRespCommandsInfo = null; + private static IReadOnlyDictionary ExternalRespCommandsInfo = null; + private static IReadOnlyDictionary BasicRespCommandsInfo = null; + private static IReadOnlyDictionary> ArrayRespCommandsInfo = null; + private static IReadOnlySet AllRespCommandNames = null; + private static IReadOnlySet ExternalRespCommandNames = null; + + private readonly RespCommandFlags flags; + private readonly RespAclCategories aclCategories; + + private readonly string[] respFormatFlags; + private readonly string[] respFormatAclCategories; + + private static bool TryInitialize(ILogger logger) { - for (int i = 0; i < RespCommandsOptionInfo.optionMap.Length; i++) + lock (IsInitializedLock) { - optionOutput = RespCommandsOptionInfo.optionMap[i]; - if (command.SequenceEqual(new ReadOnlySpan(optionOutput.name)) && this.options.Contains(optionOutput.option)) - return true; + if (IsInitialized) return true; + + IsInitialized = TryInitializeRespCommandsInfo(logger); + return IsInitialized; } - optionOutput = null; - return false; } - public static RespCommandsInfo findCommand(RespCommand cmd, byte subCmd = 0) + private static bool TryInitializeRespCommandsInfo(ILogger logger = null) { + var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource, null, + Assembly.GetExecutingAssembly()); + var commandsInfoProvider = RespCommandsInfoProviderFactory.GetRespCommandsInfoProvider(); + + var importSucceeded = commandsInfoProvider.TryImportRespCommandsInfo(RespCommandsEmbeddedFileName, + streamProvider, out var tmpAllRespCommandsInfo, logger); + + if (!importSucceeded) return false; - RespCommandsInfo result = cmd switch + var tmpBasicRespCommandsInfo = new Dictionary(); + var tmpArrayRespCommandsInfo = new Dictionary>(); + foreach (var respCommandInfo in tmpAllRespCommandsInfo.Values) { - RespCommand.SortedSet => sortedSetCommandsInfoMap.GetValueOrDefault(subCmd), - RespCommand.List => listCommandsInfoMap.GetValueOrDefault(subCmd), - RespCommand.Hash => hashCommandsInfoMap.GetValueOrDefault(subCmd), - RespCommand.Set => setCommandsInfoMap.GetValueOrDefault(subCmd), - RespCommand.All => customCommandsInfoMap.GetValueOrDefault(cmd), - _ => basicCommandsInfoMap.GetValueOrDefault(cmd) - }; - return result; + if (respCommandInfo.Command == RespCommand.NONE) continue; + + if (respCommandInfo.ArrayCommand.HasValue) + { + if (!tmpArrayRespCommandsInfo.ContainsKey(respCommandInfo.Command)) + tmpArrayRespCommandsInfo.Add(respCommandInfo.Command, new Dictionary()); + tmpArrayRespCommandsInfo[respCommandInfo.Command] + .Add(respCommandInfo.ArrayCommand.Value, respCommandInfo); + } + else + { + tmpBasicRespCommandsInfo.Add(respCommandInfo.Command, respCommandInfo); + } + } + + AllRespCommandsInfo = tmpAllRespCommandsInfo; + ExternalRespCommandsInfo = new ReadOnlyDictionary(tmpAllRespCommandsInfo + .Where(ci => !ci.Value.IsInternal) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + AllRespCommandNames = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, AllRespCommandsInfo.Keys.ToArray()); + ExternalRespCommandNames = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, ExternalRespCommandsInfo.Keys.ToArray()); + BasicRespCommandsInfo = new ReadOnlyDictionary(tmpBasicRespCommandsInfo); + ArrayRespCommandsInfo = new ReadOnlyDictionary>( + tmpArrayRespCommandsInfo + .ToDictionary(kvp => kvp.Key, + kvp => + (IReadOnlyDictionary)new ReadOnlyDictionary( + kvp.Value))); + + return true; } - private static readonly Dictionary basicCommandsInfoMap = new Dictionary - { - {RespCommand.GET, new RespCommandsInfo("GET", RespCommand.GET, 1, null)}, - {RespCommand.SET, new RespCommandsInfo("SET", RespCommand.SET, -2, new HashSet{ - RespCommandOption.EX, - RespCommandOption.NX, - RespCommandOption.XX, - RespCommandOption.GET, - RespCommandOption.PX, - RespCommandOption.EXAT, - RespCommandOption.PXAT, - })}, - {RespCommand.GETRANGE, new RespCommandsInfo("GETRANGE", RespCommand.GETRANGE, 3, null)}, - {RespCommand.SETRANGE, new RespCommandsInfo("SETRANGE", RespCommand.SETRANGE, 3, null)}, - // PUBLISH - {RespCommand.PFADD, new RespCommandsInfo("PFADD", RespCommand.PFADD, -2, null)}, - {RespCommand.PFCOUNT, new RespCommandsInfo("PFCOUNT", RespCommand.PFCOUNT, -1, null)}, - {RespCommand.PFMERGE, new RespCommandsInfo("PFMERGE", RespCommand.PFMERGE, -2, null)}, - - {RespCommand.SETEX, new RespCommandsInfo("SETEX", RespCommand.SETEX, -3, null)}, - {RespCommand.PSETEX, new RespCommandsInfo("PSETEX", RespCommand.PSETEX, 3, null)}, - {RespCommand.SETEXNX, new RespCommandsInfo("SETEXNX", RespCommand.SETEXNX, -2, null)}, - {RespCommand.SETEXXX, new RespCommandsInfo("SETEXXX", RespCommand.SETEXXX, -2, null)}, - {RespCommand.DEL, new RespCommandsInfo("DEL", RespCommand.DEL, -1, null)}, - {RespCommand.EXISTS, new RespCommandsInfo("EXISTS", RespCommand.EXISTS, 1, null)}, - {RespCommand.RENAME, new RespCommandsInfo("RENAME", RespCommand.RENAME, 2, null)}, - {RespCommand.INCR, new RespCommandsInfo("INCR", RespCommand.INCR, 1, null)}, - {RespCommand.INCRBY, new RespCommandsInfo("INCRBY", RespCommand.INCRBY, 2, null)}, - {RespCommand.DECR, new RespCommandsInfo("DECR", RespCommand.DECR, 1, null)}, - {RespCommand.DECRBY, new RespCommandsInfo("DECRBY", RespCommand.DECRBY, 2, null)}, - {RespCommand.EXPIRE, new RespCommandsInfo("EXPIRE", RespCommand.EXPIRE, -2, new HashSet{ - RespCommandOption.NX, - RespCommandOption.XX, - RespCommandOption.GT, - RespCommandOption.LT, - })}, - {RespCommand.PEXPIRE, new RespCommandsInfo("PEXPIRE", RespCommand.PEXPIRE, -2, new HashSet{ - RespCommandOption.NX, - RespCommandOption.XX, - RespCommandOption.GT, - RespCommandOption.LT, - })}, - {RespCommand.PERSIST, new RespCommandsInfo("PERSIST", RespCommand.PERSIST, 1, null)}, - {RespCommand.TTL, new RespCommandsInfo("TTL", RespCommand.TTL, 1, null)}, - {RespCommand.PTTL, new RespCommandsInfo("PTTL", RespCommand.PTTL, 1, null)}, - {RespCommand.SETBIT, new RespCommandsInfo("SETBIT", RespCommand.SETBIT, 3, null)}, - {RespCommand.GETBIT, new RespCommandsInfo("GETBIT", RespCommand.GETBIT, 2, null)}, - {RespCommand.BITCOUNT, new RespCommandsInfo("BITCOUNT", RespCommand.BITCOUNT, -1, null)}, - {RespCommand.BITPOS, new RespCommandsInfo("BITPOS", RespCommand.BITPOS, -2, null)}, - {RespCommand.BITFIELD, new RespCommandsInfo("BITFIELD", RespCommand.BITFIELD, -1, null)}, - - {RespCommand.MSET, new RespCommandsInfo("MSET", RespCommand.MSET, -2, null)}, - {RespCommand.MSETNX, new RespCommandsInfo("MSETNX", RespCommand.MSETNX, -2, null)}, - {RespCommand.MGET, new RespCommandsInfo("MGET", RespCommand.MGET, -2, null)}, - {RespCommand.UNLINK, new RespCommandsInfo("UNLINK", RespCommand.UNLINK, -1, null)}, - - {RespCommand.MULTI, new RespCommandsInfo("MULTI", RespCommand.MULTI, 0, null)}, - {RespCommand.EXEC, new RespCommandsInfo("EXEC", RespCommand.EXEC, 0, null)}, - {RespCommand.WATCH, new RespCommandsInfo("WATCH", RespCommand.WATCH, -1, null)}, - {RespCommand.UNWATCH, new RespCommandsInfo("WATCH", RespCommand.UNWATCH, 0, null)}, - {RespCommand.DISCARD, new RespCommandsInfo("DISCARD", RespCommand.DISCARD, 0, null)}, - {RespCommand.GETDEL, new RespCommandsInfo("GETDEL", RespCommand.GETDEL, 1, null)}, - {RespCommand.APPEND, new RespCommandsInfo("APPEND", RespCommand.APPEND, 2, null)}, - - //Admin Commands - {RespCommand.ECHO, new RespCommandsInfo("ECHO", RespCommand.ECHO, 1, null)}, - {RespCommand.REPLICAOF, new RespCommandsInfo("REPLICAOF", RespCommand.REPLICAOF, 2, null)}, - {RespCommand.SECONDARYOF, new RespCommandsInfo("SLAVEOF", RespCommand.SECONDARYOF, 2, null)}, - {RespCommand.CONFIG, new RespCommandsInfo("CONFIG", RespCommand.CONFIG, 1, null)}, - {RespCommand.CLIENT, new RespCommandsInfo("CLIENT", RespCommand.CLIENT, 3, null)}, - {RespCommand.REGISTERCS, new RespCommandsInfo("REGISTERCS", RespCommand.REGISTERCS, -4, null)}, - }; - - private static readonly Dictionary sortedSetCommandsInfoMap = new Dictionary + /// + /// Gets the number of commands supported by Garnet + /// + /// The count value + /// Return number of commands that are visible externally + /// Logger + /// True if initialization was successful and data was retrieved successfully + internal static bool TryGetRespCommandsInfoCount(out int count, bool externalOnly = false, ILogger logger = null) { - {(byte)SortedSetOperation.ZADD, new RespCommandsInfo("ZADD", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.ZADD)}, - {(byte)SortedSetOperation.ZMSCORE, new RespCommandsInfo("ZMSCORE", RespCommand.SortedSet, -2,null, (byte)SortedSetOperation.ZMSCORE)}, - {(byte)SortedSetOperation.ZREM, new RespCommandsInfo("ZREM", RespCommand.SortedSet, -2,null, (byte)SortedSetOperation.ZREM)}, - {(byte)SortedSetOperation.ZCARD, new RespCommandsInfo("ZCARD", RespCommand.SortedSet, 1,null, (byte)SortedSetOperation.ZCARD)}, - {(byte)SortedSetOperation.ZPOPMAX, new RespCommandsInfo("ZPOPMAX", RespCommand.SortedSet, -1,null, (byte)SortedSetOperation.ZPOPMAX)}, - {(byte)SortedSetOperation.ZSCORE, new RespCommandsInfo("ZSCORE", RespCommand.SortedSet, 2,null, (byte)SortedSetOperation.ZSCORE)}, - {(byte)SortedSetOperation.ZCOUNT, new RespCommandsInfo("ZCOUNT", RespCommand.SortedSet, 3,null, (byte)SortedSetOperation.ZCOUNT)}, - {(byte)SortedSetOperation.ZINCRBY, new RespCommandsInfo("ZINCRBY", RespCommand.SortedSet, 3,null, (byte)SortedSetOperation.ZINCRBY)}, - {(byte)SortedSetOperation.ZRANK, new RespCommandsInfo("ZRANK", RespCommand.SortedSet, 2,null, (byte)SortedSetOperation.ZRANK)}, - {(byte)SortedSetOperation.ZRANGE, new RespCommandsInfo("ZRANGE", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.ZRANGE)}, - {(byte)SortedSetOperation.ZRANGEBYSCORE, new RespCommandsInfo("ZRANGEBYSCORE", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.ZRANGEBYSCORE)}, - {(byte)SortedSetOperation.ZREVRANK, new RespCommandsInfo("ZREVRANK", RespCommand.SortedSet, 2,null, (byte)SortedSetOperation.ZREVRANK)}, - {(byte)SortedSetOperation.ZREMRANGEBYLEX, new RespCommandsInfo("ZREMRANGEBYLEX", RespCommand.SortedSet, 3,null, (byte)SortedSetOperation.ZREMRANGEBYLEX)}, - {(byte)SortedSetOperation.ZREMRANGEBYRANK, new RespCommandsInfo("ZREMRANGEBYRANK", RespCommand.SortedSet, 3,null, (byte)SortedSetOperation.ZREMRANGEBYRANK)}, - {(byte)SortedSetOperation.ZREMRANGEBYSCORE, new RespCommandsInfo("ZREMRANGEBYSCORE", RespCommand.SortedSet, 3,null, (byte)SortedSetOperation.ZREMRANGEBYSCORE)}, - {(byte)SortedSetOperation.ZLEXCOUNT, new RespCommandsInfo("ZLEXCOUNT", RespCommand.SortedSet, 3,null, (byte)SortedSetOperation.ZLEXCOUNT)}, - {(byte)SortedSetOperation.ZPOPMIN, new RespCommandsInfo("ZPOPMIN", RespCommand.SortedSet, -1,null, (byte)SortedSetOperation.ZPOPMIN)}, - {(byte)SortedSetOperation.ZRANDMEMBER, new RespCommandsInfo("ZRANDMEMBER", RespCommand.SortedSet, -1,null, (byte)SortedSetOperation.ZRANDMEMBER)}, - {(byte)SortedSetOperation.GEOADD, new RespCommandsInfo("GEOADD", RespCommand.SortedSet, -4,null, (byte)SortedSetOperation.GEOADD)}, - {(byte)SortedSetOperation.GEOHASH, new RespCommandsInfo("GEOHASH", RespCommand.SortedSet, -1,null, (byte)SortedSetOperation.GEOHASH)}, - {(byte)SortedSetOperation.GEODIST, new RespCommandsInfo("GEODIST", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.GEODIST)}, - {(byte)SortedSetOperation.GEOPOS, new RespCommandsInfo("GEOPOS", RespCommand.SortedSet, -1,null, (byte)SortedSetOperation.GEOPOS)}, - {(byte)SortedSetOperation.GEOSEARCH, new RespCommandsInfo("GEOSEARCH", RespCommand.SortedSet, -6,null, (byte)SortedSetOperation.GEOSEARCH)}, - {(byte)SortedSetOperation.ZREVRANGE, new RespCommandsInfo("ZREVRANGE", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.ZREVRANGE)}, - {(byte)SortedSetOperation.ZSCAN, new RespCommandsInfo("ZSCAN", RespCommand.SortedSet, -2,null, (byte)SortedSetOperation.ZSCAN)}, - }; - - private static readonly Dictionary listCommandsInfoMap = new Dictionary + count = -1; + if (!IsInitialized && !TryInitialize(logger)) return false; + + count = externalOnly ? ExternalRespCommandsInfo!.Count : AllRespCommandsInfo!.Count; + return true; + } + + /// + /// Gets all the command info objects of commands supported by Garnet + /// + /// Mapping between command name to command info + /// Return only commands that are visible externally + /// Logger + /// True if initialization was successful and data was retrieved successfully + public static bool TryGetRespCommandsInfo(out IReadOnlyDictionary respCommandsInfo, bool externalOnly = false, ILogger logger = null) { - {(byte)ListOperation.LPUSH, new RespCommandsInfo("LPUSH", RespCommand.List, -2, null, (byte)ListOperation.LPUSH)}, - {(byte)ListOperation.LPOP, new RespCommandsInfo("LPOP", RespCommand.List, -1, null, (byte)ListOperation.LPOP)}, - {(byte)ListOperation.RPUSH, new RespCommandsInfo("RPUSH", RespCommand.List, -2, null, (byte)ListOperation.RPUSH)}, - {(byte)ListOperation.RPOP, new RespCommandsInfo("RPOP", RespCommand.List, -1, null, (byte)ListOperation.RPOP)}, - {(byte)ListOperation.LLEN, new RespCommandsInfo("LLEN", RespCommand.List, 1, null, (byte)ListOperation.LLEN)}, - {(byte)ListOperation.LTRIM, new RespCommandsInfo("LTRIM", RespCommand.List, 3, null, (byte)ListOperation.LTRIM)}, - {(byte)ListOperation.LRANGE, new RespCommandsInfo("LRANGE", RespCommand.List, 3, null, (byte)ListOperation.LRANGE)}, - {(byte)ListOperation.LINDEX, new RespCommandsInfo("LINDEX", RespCommand.List, 2, null, (byte)ListOperation.LINDEX)}, - {(byte)ListOperation.LINSERT, new RespCommandsInfo("LINSERT", RespCommand.List, 4, null, (byte)ListOperation.LINSERT)}, - {(byte)ListOperation.LREM, new RespCommandsInfo("LREM", RespCommand.List, 3, null, (byte)ListOperation.LREM) }, - {(byte)ListOperation.LSET, new RespCommandsInfo("LSET", RespCommand.List, 3, null, (byte)ListOperation.LSET) }, - }; - - private static readonly Dictionary hashCommandsInfoMap = new Dictionary + respCommandsInfo = default; + if (!IsInitialized && !TryInitialize(logger)) return false; + + respCommandsInfo = externalOnly ? ExternalRespCommandsInfo : AllRespCommandsInfo; + return true; + } + + /// + /// Gets all the command names of commands supported by Garnet + /// + /// The command names + /// Return only names of commands that are visible externally + /// Logger + /// True if initialization was successful and data was retrieved successfully + public static bool TryGetRespCommandNames(out IReadOnlySet respCommandNames, bool externalOnly = false, ILogger logger = null) { - {(byte)HashOperation.HSET, new RespCommandsInfo("HSET", RespCommand.Hash, -3, null, (byte)HashOperation.HSET) }, - {(byte)HashOperation.HMSET, new RespCommandsInfo("HMSET", RespCommand.Hash, -3, null, (byte)HashOperation.HMSET)}, - {(byte)HashOperation.HGET, new RespCommandsInfo("HGET", RespCommand.Hash, 2, null, (byte)HashOperation.HGET)}, - {(byte)HashOperation.HMGET, new RespCommandsInfo("HMGET", RespCommand.Hash, -2, null, (byte)HashOperation.HMGET)}, - {(byte)HashOperation.HGETALL, new RespCommandsInfo("HGETALL", RespCommand.Hash, 1, null, (byte)HashOperation.HGETALL)}, - {(byte)HashOperation.HDEL, new RespCommandsInfo("HDEL", RespCommand.Hash, -2, null, (byte)HashOperation.HDEL)}, - {(byte)HashOperation.HLEN, new RespCommandsInfo("HLEN", RespCommand.Hash, 1, null, (byte)HashOperation.HLEN)}, - {(byte)HashOperation.HEXISTS, new RespCommandsInfo("HEXISTS", RespCommand.Hash, 2, null, (byte)HashOperation.HEXISTS)}, - {(byte)HashOperation.HKEYS, new RespCommandsInfo("HKEYS", RespCommand.Hash, 1, null, (byte)HashOperation.HKEYS)}, - {(byte)HashOperation.HVALS, new RespCommandsInfo("HVALS", RespCommand.Hash, 1, null, (byte)HashOperation.HVALS)}, - {(byte)HashOperation.HINCRBY, new RespCommandsInfo("HINCRBY", RespCommand.Hash, 3, null, (byte)HashOperation.HINCRBY)}, - {(byte)HashOperation.HINCRBYFLOAT, new RespCommandsInfo("HINCRBYFLOAT", RespCommand.Hash, 3, null, (byte)HashOperation.HINCRBYFLOAT)}, - {(byte)HashOperation.HSETNX, new RespCommandsInfo("HSETNX", RespCommand.Hash, 3, null, (byte)HashOperation.HSETNX)}, - {(byte)HashOperation.HRANDFIELD, new RespCommandsInfo("HRANDFIELD", RespCommand.Hash, -1, null, (byte)HashOperation.HRANDFIELD)}, - {(byte)HashOperation.HSCAN, new RespCommandsInfo("HSCAN", RespCommand.Hash, -2, null, (byte)HashOperation.HSCAN)}, - {(byte)HashOperation.HSTRLEN, new RespCommandsInfo("HSTRLEN", RespCommand.Hash, 2, null, (byte)HashOperation.HSTRLEN)}, - }; - - private static readonly Dictionary setCommandsInfoMap = new Dictionary + respCommandNames = default; + if (!IsInitialized && !TryInitialize(logger)) return false; + + respCommandNames = externalOnly ? ExternalRespCommandNames : AllRespCommandNames; + return true; + } + + /// + /// Gets command info by command name + /// + /// The command name + /// The command info + /// Logger + /// True if initialization was successful and command info was found + internal static bool TryGetRespCommandInfo(string cmdName, out RespCommandsInfo respCommandsInfo, ILogger logger = null) { - {(byte)SetOperation.SADD, new RespCommandsInfo("SADD", RespCommand.Set, -2, null, (byte)SetOperation.SADD)}, - {(byte)SetOperation.SMEMBERS, new RespCommandsInfo("SMEMBERS", RespCommand.Set, 1, null, (byte)SetOperation.SMEMBERS)}, - {(byte)SetOperation.SREM, new RespCommandsInfo("SREM", RespCommand.Set, -2, null, (byte)SetOperation.SREM)}, - {(byte)SetOperation.SCARD, new RespCommandsInfo("SCARD", RespCommand.Set, 1, null, (byte)SetOperation.SCARD)}, - {(byte)SetOperation.SRANDMEMBER, new RespCommandsInfo("SRANDMEMBER", RespCommand.Set, -2, null, (byte)SetOperation.SRANDMEMBER)}, - {(byte)SetOperation.SPOP, new RespCommandsInfo("SPOP", RespCommand.Set, -1, null, (byte)SetOperation.SPOP) }, - {(byte)SetOperation.SSCAN, new RespCommandsInfo("SSCAN", RespCommand.Set, -2, null, (byte)SetOperation.SSCAN) }, - {(byte)SetOperation.SMOVE, new RespCommandsInfo("SMOVE", RespCommand.Set, 3, null, (byte)SetOperation.SMOVE) }, - {(byte)SetOperation.SISMEMBER, new RespCommandsInfo("SISMEMBER", RespCommand.Set, 2, null, (byte)SetOperation.SISMEMBER) }, - {(byte)SetOperation.SUNION, new RespCommandsInfo("SUNION", RespCommand.Set, -1, null, (byte)SetOperation.SUNION) }, - {(byte)SetOperation.SUNIONSTORE, new RespCommandsInfo("SUNIONSTORE", RespCommand.Set, -2, null, (byte)SetOperation.SUNIONSTORE) }, - {(byte)SetOperation.SDIFF, new RespCommandsInfo("SDIFF", RespCommand.Set, -1, null, (byte)SetOperation.SDIFF) }, - {(byte)SetOperation.SDIFFSTORE, new RespCommandsInfo("SDIFFSTORE", RespCommand.Set, -2, null, (byte)SetOperation.SDIFFSTORE) } - }; - - private static readonly Dictionary customCommandsInfoMap = new Dictionary + respCommandsInfo = default; + if ((!IsInitialized && !TryInitialize(logger)) || + !AllRespCommandsInfo.ContainsKey(cmdName)) return false; + + respCommandsInfo = AllRespCommandsInfo[cmdName]; + return true; + } + + /// + /// Gets command info by RespCommand enum and sub-command byte, if applicable + /// + /// The RespCommand enum + /// Logger + /// The commands info + /// The sub-command byte, if applicable + /// Return only commands that are allowed in a transaction context (False by default) + /// True if initialization was successful and command info was found + internal static bool TryGetRespCommandInfo(RespCommand cmd, + out RespCommandsInfo respCommandsInfo, byte subCmd = 0, bool txnOnly = false, ILogger logger = null) { - {RespCommand.COSCAN, new RespCommandsInfo("COSCAN", RespCommand.All, -2, null, (byte)RespCommand.COSCAN) }, - }; - } + respCommandsInfo = default; + if (!IsInitialized && !TryInitialize(logger)) return false; - /// - /// Container for commands option information - /// - class RespCommandsOptionInfo - { - public readonly string nameStr; - public readonly int arity; - public readonly byte[] name; - public readonly RespCommandOption option; + RespCommandsInfo tmpRespCommandInfo = default; + if (ArrayRespCommandsInfo.ContainsKey(cmd) && ArrayRespCommandsInfo[cmd].ContainsKey(subCmd)) + tmpRespCommandInfo = ArrayRespCommandsInfo[cmd][subCmd]; + else if (BasicRespCommandsInfo.ContainsKey(cmd)) + tmpRespCommandInfo = BasicRespCommandsInfo[cmd]; + if (tmpRespCommandInfo == default || + (txnOnly && tmpRespCommandInfo.Flags.HasFlag(RespCommandFlags.NoMulti))) return false; - public RespCommandsOptionInfo(string name, RespCommandOption opt, int ariry) - { - nameStr = name.ToUpperInvariant(); - this.name = System.Text.Encoding.ASCII.GetBytes(nameStr); - this.option = opt; - this.arity = ariry; + respCommandsInfo = tmpRespCommandInfo; + return true; } - public static readonly RespCommandsOptionInfo[] optionMap = new RespCommandsOptionInfo[] + /// + /// Serializes the current object to RESP format + /// + /// Serialized value + public string ToRespFormat() { - new RespCommandsOptionInfo("EX" ,RespCommandOption.EX, 2), - new RespCommandsOptionInfo("NX" ,RespCommandOption.NX, 1), - new RespCommandsOptionInfo("XX" ,RespCommandOption.XX, 1), - new RespCommandsOptionInfo("GET" ,RespCommandOption.GET, 1), - new RespCommandsOptionInfo("PX" ,RespCommandOption.PX, 2), - new RespCommandsOptionInfo("EXAT" ,RespCommandOption.EXAT, 2), - new RespCommandsOptionInfo("PXAT" ,RespCommandOption.PXAT, 2), - new RespCommandsOptionInfo("PERSIST" ,RespCommandOption.PERSIST, 1), - new RespCommandsOptionInfo("GT" ,RespCommandOption.GT, 1), - new RespCommandsOptionInfo("LT" ,RespCommandOption.LT, 1), - }; + var sb = new StringBuilder(); + + sb.Append("*10\r\n"); + // 1) Name + sb.Append($"${this.Name.Length}\r\n{this.Name}\r\n"); + // 2) Arity + sb.Append($":{this.Arity}\r\n"); + // 3) Flags + sb.Append($"*{this.respFormatFlags.Length}\r\n"); + foreach (var flag in this.respFormatFlags) + sb.Append($"+{flag}\r\n"); + // 4) First key + sb.Append($":{this.FirstKey}\r\n"); + // 5) Last key + sb.Append($":{this.LastKey}\r\n"); + // 6) Step + sb.Append($":{this.Step}\r\n"); + // 7) ACL categories + sb.Append($"*{this.respFormatAclCategories.Length}\r\n"); + foreach (var aclCat in this.respFormatAclCategories) + sb.Append($"+@{aclCat}\r\n"); + // 8) Tips + var tipCount = this.Tips?.Length ?? 0; + sb.Append($"*{tipCount}\r\n"); + if (this.Tips != null && tipCount > 0) + { + foreach (var tip in this.Tips) + sb.Append($"${tip.Length}\r\n{tip}\r\n"); + } + + // 9) Key specifications + var ksCount = this.KeySpecifications?.Length ?? 0; + sb.Append($"*{ksCount}\r\n"); + if (this.KeySpecifications != null && ksCount > 0) + { + foreach (var ks in this.KeySpecifications) + sb.Append(ks.RespFormat); + } + + // 10) SubCommands + var subCommandCount = this.SubCommands?.Length ?? 0; + sb.Append($"*{subCommandCount}\r\n"); + if (this.SubCommands != null && subCommandCount > 0) + { + foreach (var subCommand in SubCommands) + sb.Append(subCommand.RespFormat); + } + + return sb.ToString(); + } } } \ No newline at end of file diff --git a/libs/server/Resp/RespCommandsInfo.json b/libs/server/Resp/RespCommandsInfo.json new file mode 100644 index 0000000000..0b762e6c63 --- /dev/null +++ b/libs/server/Resp/RespCommandsInfo.json @@ -0,0 +1,4770 @@ +[ + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": [ + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|CAT", + "IsInternal": false, + "Arity": -2, + "Flags": "Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|DELUSER", + "IsInternal": false, + "Arity": -3, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "request_policy:all_nodes", + "response_policy:all_succeeded" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|LIST", + "IsInternal": false, + "Arity": 2, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|LOAD", + "IsInternal": false, + "Arity": 2, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|SETUSER", + "IsInternal": false, + "Arity": -3, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "request_policy:all_nodes", + "response_policy:all_succeeded" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|USERS", + "IsInternal": false, + "Arity": 2, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ACL", + "ArrayCommand": 0, + "Name": "ACL|WHOAMI", + "IsInternal": false, + "Arity": 2, + "Flags": "Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + } + ] + }, + { + "Command": "APPEND", + "ArrayCommand": null, + "Name": "APPEND", + "IsInternal": false, + "Arity": 3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "ASKING", + "ArrayCommand": null, + "Name": "ASKING", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "AUTH", + "ArrayCommand": null, + "Name": "AUTH", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Loading, NoAuth, NoScript, Stale, AllowBusy", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "BGSAVE", + "ArrayCommand": null, + "Name": "BGSAVE", + "IsInternal": false, + "Arity": -1, + "Flags": "Admin, NoAsyncLoading, NoScript", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "BITCOUNT", + "ArrayCommand": null, + "Name": "BITCOUNT", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Bitmap, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "BITFIELD", + "ArrayCommand": null, + "Name": "BITFIELD", + "IsInternal": false, + "Arity": -2, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Bitmap, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": "This command allows both access and modification of the key", + "Flags": "RW, Access, Update, VariableFlags" + } + ], + "SubCommands": null + }, + { + "Command": "BITFIELD_RO", + "ArrayCommand": null, + "Name": "BITFIELD_RO", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Bitmap, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "BITOP", + "ArrayCommand": null, + "Name": "BITOP", + "IsInternal": false, + "Arity": -4, + "Flags": "DenyOom, Write", + "FirstKey": 2, + "LastKey": -1, + "Step": 1, + "AclCategories": "Bitmap, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 3 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "BITPOS", + "ArrayCommand": null, + "Name": "BITPOS", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Bitmap, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "CLIENT", + "ArrayCommand": null, + "Name": "CLIENT", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": [ + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|ADDSLOTS", + "IsInternal": false, + "Arity": -3, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|ADDSLOTSRANGE", + "IsInternal": false, + "Arity": -4, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|BUMPEPOCH", + "IsInternal": false, + "Arity": 2, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|COUNTKEYSINSLOT", + "IsInternal": false, + "Arity": 3, + "Flags": "Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|DELSLOTS", + "IsInternal": false, + "Arity": -3, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|DELSLOTSRANGE", + "IsInternal": false, + "Arity": -4, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|FAILOVER", + "IsInternal": false, + "Arity": -2, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|FORGET", + "IsInternal": false, + "Arity": 3, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|GETKEYSINSLOT", + "IsInternal": false, + "Arity": 4, + "Flags": "Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|INFO", + "IsInternal": false, + "Arity": 2, + "Flags": "Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|KEYSLOT", + "IsInternal": false, + "Arity": 3, + "Flags": "Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|MEET", + "IsInternal": false, + "Arity": -4, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|MYID", + "IsInternal": false, + "Arity": 2, + "Flags": "Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|NODES", + "IsInternal": false, + "Arity": 2, + "Flags": "Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|REPLICAS", + "IsInternal": false, + "Arity": 3, + "Flags": "Admin, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|REPLICATE", + "IsInternal": false, + "Arity": 3, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|RESET", + "IsInternal": false, + "Arity": -2, + "Flags": "Admin, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|SET-CONFIG-EPOCH", + "IsInternal": false, + "Arity": 3, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|SETSLOT", + "IsInternal": false, + "Arity": -4, + "Flags": "Admin, NoAsyncLoading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CLUSTER", + "ArrayCommand": 0, + "Name": "CLUSTER|SLOTS", + "IsInternal": false, + "Arity": 2, + "Flags": "Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + } + ] + }, + { + "Command": "COMMAND", + "ArrayCommand": 0, + "Name": "COMMAND", + "IsInternal": false, + "Arity": -1, + "Flags": "Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": null, + "SubCommands": [ + { + "Command": "COMMAND", + "ArrayCommand": 0, + "Name": "COMMAND|COUNT", + "IsInternal": false, + "Arity": 2, + "Flags": "Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "COMMAND", + "ArrayCommand": 0, + "Name": "COMMAND|INFO", + "IsInternal": false, + "Arity": -2, + "Flags": "Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": null, + "SubCommands": null + } + ] + }, + { + "Command": "COMMITAOF", + "ArrayCommand": null, + "Name": "COMMITAOF", + "IsInternal": false, + "Arity": -1, + "Flags": "Admin, NoMulti, NoScript, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CONFIG", + "ArrayCommand": 0, + "Name": "CONFIG", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": [ + { + "Command": "CONFIG", + "ArrayCommand": 0, + "Name": "CONFIG|GET", + "IsInternal": false, + "Arity": -3, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CONFIG", + "ArrayCommand": 0, + "Name": "CONFIG|REWRITE", + "IsInternal": false, + "Arity": 2, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "request_policy:all_nodes", + "response_policy:all_succeeded" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "CONFIG", + "ArrayCommand": 0, + "Name": "CONFIG|SET", + "IsInternal": false, + "Arity": -4, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "request_policy:all_nodes", + "response_policy:all_succeeded" + ], + "KeySpecifications": null, + "SubCommands": null + } + ] + }, + { + "Command": "All", + "ArrayCommand": 39, + "Name": "COSCAN", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "DBSIZE", + "ArrayCommand": null, + "Name": "DBSIZE", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, ReadOnly", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Fast, KeySpace, Read", + "Tips": [ + "request_policy:all_shards", + "response_policy:agg_sum" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "DECR", + "ArrayCommand": null, + "Name": "DECR", + "IsInternal": false, + "Arity": 2, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "DECRBY", + "ArrayCommand": null, + "Name": "DECRBY", + "IsInternal": false, + "Arity": 3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "DEL", + "ArrayCommand": null, + "Name": "DEL", + "IsInternal": false, + "Arity": -2, + "Flags": "Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "KeySpace, Slow, Write", + "Tips": [ + "request_policy:multi_shard", + "response_policy:agg_sum" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RM, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "DISCARD", + "ArrayCommand": null, + "Name": "DISCARD", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "ECHO", + "ArrayCommand": null, + "Name": "ECHO", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "EXEC", + "ArrayCommand": null, + "Name": "EXEC", + "IsInternal": false, + "Arity": 1, + "Flags": "Loading, NoScript, SkipSlowLog, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "EXISTS", + "ArrayCommand": null, + "Name": "EXISTS", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Read", + "Tips": [ + "request_policy:multi_shard", + "response_policy:agg_sum" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "EXPIRE", + "ArrayCommand": null, + "Name": "EXPIRE", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "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, Update" + } + ], + "SubCommands": null + }, + { + "Command": "FAILOVER", + "ArrayCommand": null, + "Name": "FAILOVER", + "IsInternal": false, + "Arity": -1, + "Flags": "Admin, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "FLUSHDB", + "ArrayCommand": null, + "Name": "FLUSHDB", + "IsInternal": false, + "Arity": -1, + "Flags": "Write", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Dangerous, KeySpace, Slow, Write", + "Tips": [ + "request_policy:all_shards", + "response_policy:all_succeeded" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "FORCEGC", + "ArrayCommand": null, + "Name": "FORCEGC", + "IsInternal": false, + "Arity": -1, + "Flags": "Admin, NoMulti, NoScript, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 10, + "Name": "GEOADD", + "IsInternal": false, + "Arity": -5, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 12, + "Name": "GEODIST", + "IsInternal": false, + "Arity": -4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 11, + "Name": "GEOHASH", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 13, + "Name": "GEOPOS", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 14, + "Name": "GEOSEARCH", + "IsInternal": false, + "Arity": -7, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "GET", + "ArrayCommand": null, + "Name": "GET", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, String", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "GETBIT", + "ArrayCommand": null, + "Name": "GETBIT", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Bitmap, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "GETDEL", + "ArrayCommand": null, + "Name": "GETDEL", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "GETRANGE", + "ArrayCommand": null, + "Name": "GETRANGE", + "IsInternal": false, + "Arity": 4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, Slow, String", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 6, + "Name": "HDEL", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 7, + "Name": "HEXISTS", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 0, + "Name": "HGET", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 8, + "Name": "HGETALL", + "IsInternal": false, + "Arity": 2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Read, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 11, + "Name": "HINCRBY", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 12, + "Name": "HINCRBYFLOAT", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 9, + "Name": "HKEYS", + "IsInternal": false, + "Arity": 2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Read, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 5, + "Name": "HLEN", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 1, + "Name": "HMGET", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 3, + "Name": "HMSET", + "IsInternal": false, + "Arity": -4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 13, + "Name": "HRANDFIELD", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Read, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 14, + "Name": "HSCAN", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Read, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 2, + "Name": "HSET", + "IsInternal": false, + "Arity": -4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 4, + "Name": "HSETNX", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 15, + "Name": "HSTRLEN", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Fast, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "Hash", + "ArrayCommand": 10, + "Name": "HVALS", + "IsInternal": false, + "Arity": 2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Hash, Read, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "INCR", + "ArrayCommand": null, + "Name": "INCR", + "IsInternal": false, + "Arity": 2, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "INCRBY", + "ArrayCommand": null, + "Name": "INCRBY", + "IsInternal": false, + "Arity": 3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "INFO", + "ArrayCommand": null, + "Name": "INFO", + "IsInternal": false, + "Arity": -1, + "Flags": "Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Dangerous, Slow", + "Tips": [ + "nondeterministic_output", + "request_policy:all_shards", + "response_policy:special" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "KEYS", + "ArrayCommand": null, + "Name": "KEYS", + "IsInternal": false, + "Arity": 2, + "Flags": "ReadOnly", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Dangerous, KeySpace, Read, Slow", + "Tips": [ + "request_policy:all_shards", + "nondeterministic_output_order" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "LASTSAVE", + "ArrayCommand": null, + "Name": "LASTSAVE", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Fast", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "LATENCY", + "ArrayCommand": 0, + "Name": "LATENCY", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": [ + { + "Command": "LATENCY", + "ArrayCommand": 0, + "Name": "LATENCY|HISTOGRAM", + "IsInternal": false, + "Arity": -2, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "nondeterministic_output", + "request_policy:all_nodes", + "response_policy:special" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "LATENCY", + "ArrayCommand": 0, + "Name": "LATENCY|RESET", + "IsInternal": false, + "Arity": -2, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": [ + "request_policy:all_nodes", + "response_policy:agg_sum" + ], + "KeySpecifications": null, + "SubCommands": null + } + ] + }, + { + "Command": "List", + "ArrayCommand": 9, + "Name": "LINDEX", + "IsInternal": false, + "Arity": 3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 10, + "Name": "LINSERT", + "IsInternal": false, + "Arity": 5, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 6, + "Name": "LLEN", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 13, + "Name": "LMOVE", + "IsInternal": false, + "Arity": 5, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 2, + "Step": 1, + "AclCategories": "List, Slow, 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": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 0, + "Name": "LPOP", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 1, + "Name": "LPUSH", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 2, + "Name": "LPUSHX", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 8, + "Name": "LRANGE", + "IsInternal": false, + "Arity": 4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 11, + "Name": "LREM", + "IsInternal": false, + "Arity": 4, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 14, + "Name": "LSET", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 7, + "Name": "LTRIM", + "IsInternal": false, + "Arity": 4, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "List, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "MEMORY", + "ArrayCommand": 0, + "Name": "MEMORY", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": [ + { + "Command": "MEMORY", + "ArrayCommand": 0, + "Name": "MEMORY|USAGE", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 2, + "LastKey": 2, + "Step": 1, + "AclCategories": "Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + } + ] + }, + { + "Command": "MGET", + "ArrayCommand": null, + "Name": "MGET", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, Read, String", + "Tips": [ + "request_policy:multi_shard" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "MIGRATE", + "ArrayCommand": null, + "Name": "MIGRATE", + "IsInternal": false, + "Arity": -6, + "Flags": "MovableKeys, Write", + "FirstKey": 3, + "LastKey": 3, + "Step": 1, + "AclCategories": "Dangerous, KeySpace, Slow, Write", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 3 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchKeyword", + "Keyword": "KEYS", + "StartFrom": -2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete, Incomplete" + } + ], + "SubCommands": null + }, + { + "Command": "MODULE", + "ArrayCommand": null, + "Name": "MODULE", + "IsInternal": false, + "Arity": -2, + "Flags": "None", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "MONITOR", + "ArrayCommand": null, + "Name": "MONITOR", + "IsInternal": false, + "Arity": 1, + "Flags": "Admin, Loading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "MSET", + "ArrayCommand": null, + "Name": "MSET", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 2, + "AclCategories": "Slow, String, Write", + "Tips": [ + "request_policy:multi_shard", + "response_policy:all_succeeded" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 2, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "MSETNX", + "ArrayCommand": null, + "Name": "MSETNX", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 2, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 2, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "MULTI", + "ArrayCommand": null, + "Name": "MULTI", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "PERSIST", + "ArrayCommand": null, + "Name": "PERSIST", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "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, Update" + } + ], + "SubCommands": null + }, + { + "Command": "PEXPIRE", + "ArrayCommand": null, + "Name": "PEXPIRE", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "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, Update" + } + ], + "SubCommands": null + }, + { + "Command": "PFADD", + "ArrayCommand": null, + "Name": "PFADD", + "IsInternal": false, + "Arity": -2, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "HyperLogLog, Fast, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "PFCOUNT", + "ArrayCommand": null, + "Name": "PFCOUNT", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "HyperLogLog, Read, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": "RW because it may change the internal representation of the key, and propagate to replicas", + "Flags": "RW, Access" + } + ], + "SubCommands": null + }, + { + "Command": "PFMERGE", + "ArrayCommand": null, + "Name": "PFMERGE", + "IsInternal": false, + "Arity": -2, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "HyperLogLog, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Insert" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "PING", + "ArrayCommand": null, + "Name": "PING", + "IsInternal": false, + "Arity": -1, + "Flags": "Fast", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": [ + "request_policy:all_shards", + "response_policy:all_succeeded" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "PSETEX", + "ArrayCommand": null, + "Name": "PSETEX", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "PSUBSCRIBE", + "ArrayCommand": null, + "Name": "PSUBSCRIBE", + "IsInternal": false, + "Arity": -2, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "PubSub, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "PTTL", + "ArrayCommand": null, + "Name": "PTTL", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Read", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "PUBLISH", + "ArrayCommand": null, + "Name": "PUBLISH", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, Loading, PubSub, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Fast, PubSub", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "PUNSUBSCRIBE", + "ArrayCommand": null, + "Name": "PUNSUBSCRIBE", + "IsInternal": false, + "Arity": -1, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "PubSub, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "QUIT", + "ArrayCommand": null, + "Name": "QUIT", + "IsInternal": false, + "Arity": -1, + "Flags": "Fast, Loading, NoAuth, NoScript, Stale, AllowBusy", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "READONLY", + "ArrayCommand": null, + "Name": "READONLY", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "READWRITE", + "ArrayCommand": null, + "Name": "READWRITE", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "REGISTERCS", + "ArrayCommand": null, + "Name": "REGISTERCS", + "IsInternal": false, + "Arity": -5, + "Flags": "Admin, NoMulti, NoScript, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin, Dangerous", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "RENAME", + "ArrayCommand": null, + "Name": "RENAME", + "IsInternal": false, + "Arity": 3, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 2, + "Step": 1, + "AclCategories": "KeySpace, Slow, 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, Update" + } + ], + "SubCommands": null + }, + { + "Command": "REPLICAOF", + "ArrayCommand": null, + "Name": "REPLICAOF", + "IsInternal": false, + "Arity": 3, + "Flags": "Admin, NoAsyncLoading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "RESET", + "ArrayCommand": null, + "Name": "RESET", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, NoAuth, NoScript, Stale, AllowBusy", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 3, + "Name": "RPOP", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 12, + "Name": "RPOPLPUSH", + "IsInternal": false, + "Arity": 3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 2, + "Step": 1, + "AclCategories": "List, Slow, 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": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 4, + "Name": "RPUSH", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "List", + "ArrayCommand": 5, + "Name": "RPUSHX", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, List, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "RUNTXP", + "ArrayCommand": null, + "Name": "RUNTXP", + "IsInternal": false, + "Arity": -2, + "Flags": "NoMulti, NoScript", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 0, + "Name": "SADD", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Set, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "SAVE", + "ArrayCommand": null, + "Name": "SAVE", + "IsInternal": false, + "Arity": 1, + "Flags": "Admin, NoAsyncLoading, NoMulti, NoScript", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SCAN", + "ArrayCommand": null, + "Name": "SCAN", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "KeySpace, Read, Slow", + "Tips": [ + "nondeterministic_output", + "request_policy:special", + "response_policy:special" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 4, + "Name": "SCARD", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, Set", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 11, + "Name": "SDIFF", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Read, Set, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 12, + "Name": "SDIFFSTORE", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Set, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SELECT", + "ArrayCommand": null, + "Name": "SELECT", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Connection, Fast", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SET", + "ArrayCommand": null, + "Name": "SET", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": "RW and ACCESS due to the optional \u0060GET\u0060 argument", + "Flags": "RW, Access, Update, VariableFlags" + } + ], + "SubCommands": null + }, + { + "Command": "SETBIT", + "ArrayCommand": null, + "Name": "SETBIT", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Bitmap, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "SETEX", + "ArrayCommand": null, + "Name": "SETEX", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "SETEXNX", + "ArrayCommand": null, + "Name": "SETEXNX", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETEXXX", + "ArrayCommand": null, + "Name": "SETEXXX", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETKEEPTTL", + "ArrayCommand": null, + "Name": "SETKEEPTTL", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETKEEPTTLXX", + "ArrayCommand": null, + "Name": "SETKEEPTTLXX", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETRANGE", + "ArrayCommand": null, + "Name": "SETRANGE", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 8, + "Name": "SISMEMBER", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, Set", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "SECONDARYOF", + "ArrayCommand": null, + "Name": "SLAVEOF", + "IsInternal": false, + "Arity": 3, + "Flags": "Admin, NoAsyncLoading, NoScript, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Admin, Dangerous, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 3, + "Name": "SMEMBERS", + "IsInternal": false, + "Arity": 2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, Set, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 6, + "Name": "SMOVE", + "IsInternal": false, + "Arity": 4, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 2, + "Step": 1, + "AclCategories": "Fast, Set, 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": "RW, Insert" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 2, + "Name": "SPOP", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Set, Write", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 7, + "Name": "SRANDMEMBER", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, Set, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 1, + "Name": "SREM", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Set, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 5, + "Name": "SSCAN", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, Set, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "STRLEN", + "ArrayCommand": null, + "Name": "STRLEN", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, String", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "SUBSCRIBE", + "ArrayCommand": null, + "Name": "SUBSCRIBE", + "IsInternal": false, + "Arity": -2, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "PubSub, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 9, + "Name": "SUNION", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Read, Set, Slow", + "Tips": [ + "nondeterministic_output_order" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "Set", + "ArrayCommand": 10, + "Name": "SUNIONSTORE", + "IsInternal": false, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Set, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "OW, Update" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 2 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "TIME", + "ArrayCommand": null, + "Name": "TIME", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Fast", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "TTL", + "ArrayCommand": null, + "Name": "TTL", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Read", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "TYPE", + "ArrayCommand": null, + "Name": "TYPE", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Read", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "UNLINK", + "ArrayCommand": null, + "Name": "UNLINK", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, KeySpace, Write", + "Tips": [ + "request_policy:multi_shard", + "response_policy:agg_sum" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RM, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "UNSUBSCRIBE", + "ArrayCommand": null, + "Name": "UNSUBSCRIBE", + "IsInternal": false, + "Arity": -1, + "Flags": "Loading, NoScript, PubSub, Stale", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "PubSub, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "UNWATCH", + "ArrayCommand": null, + "Name": "UNWATCH", + "IsInternal": false, + "Arity": 1, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "WATCH", + "ArrayCommand": null, + "Name": "WATCH", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": -1, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "WATCHMS", + "ArrayCommand": null, + "Name": "WATCHMS", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "WATCHOS", + "ArrayCommand": null, + "Name": "WATCHOS", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 0, + "Name": "ZADD", + "IsInternal": false, + "Arity": -4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, SortedSet, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Update" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 1, + "Name": "ZCARD", + "IsInternal": false, + "Arity": 2, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 5, + "Name": "ZCOUNT", + "IsInternal": false, + "Arity": 4, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 23, + "Name": "ZDIFF", + "IsInternal": false, + "Arity": -3, + "Flags": "MovableKeys, ReadOnly", + "FirstKey": 0, + "LastKey": 0, + "Step": 0, + "AclCategories": "Read, SortedSet, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysKeyNum", + "KeyNumIdx": 0, + "FirstKey": 1, + "KeyStep": 1 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 6, + "Name": "ZINCRBY", + "IsInternal": false, + "Arity": 4, + "Flags": "DenyOom, Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, SortedSet, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Update" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 20, + "Name": "ZLEXCOUNT", + "IsInternal": false, + "Arity": 4, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 25, + "Name": "ZMSCORE", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 2, + "Name": "ZPOPMAX", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, SortedSet, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 21, + "Name": "ZPOPMIN", + "IsInternal": false, + "Arity": -2, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, SortedSet, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Access, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 22, + "Name": "ZRANDMEMBER", + "IsInternal": false, + "Arity": -2, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, SortedSet, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 8, + "Name": "ZRANGE", + "IsInternal": false, + "Arity": -4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, SortedSet, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 9, + "Name": "ZRANGEBYSCORE", + "IsInternal": false, + "Arity": -4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, SortedSet, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 7, + "Name": "ZRANK", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 4, + "Name": "ZREM", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, SortedSet, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 17, + "Name": "ZREMRANGEBYLEX", + "IsInternal": false, + "Arity": 4, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "SortedSet, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 18, + "Name": "ZREMRANGEBYRANK", + "IsInternal": false, + "Arity": 4, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "SortedSet, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 19, + "Name": "ZREMRANGEBYSCORE", + "IsInternal": false, + "Arity": 4, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "SortedSet, Slow, Write", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RW, Delete" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 15, + "Name": "ZREVRANGE", + "IsInternal": false, + "Arity": -4, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, SortedSet, Slow", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 16, + "Name": "ZREVRANK", + "IsInternal": false, + "Arity": -3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 24, + "Name": "ZSCAN", + "IsInternal": false, + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, SortedSet, Slow", + "Tips": [ + "nondeterministic_output" + ], + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + }, + { + "Command": "SortedSet", + "ArrayCommand": 3, + "Name": "ZSCORE", + "IsInternal": false, + "Arity": 3, + "Flags": "Fast, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, Read, SortedSet", + "Tips": null, + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Notes": null, + "Flags": "RO, Access" + } + ], + "SubCommands": null + } +] \ No newline at end of file diff --git a/libs/server/Resp/RespCommandsInfoProvider.cs b/libs/server/Resp/RespCommandsInfoProvider.cs new file mode 100644 index 0000000000..f260b142d2 --- /dev/null +++ b/libs/server/Resp/RespCommandsInfoProvider.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Garnet.common; +using Microsoft.Extensions.Logging; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Garnet.server +{ + /// + /// Interface for importing / exporting RESP commands info from different file types + /// + public interface IRespCommandsInfoProvider + { + /// + /// Import RESP commands info from path using a stream provider + /// + /// Path to the file containing the serialized RESP commands info + /// Stream provider to use when reading from the path + /// Logger + /// Outputs a read-only dictionary that maps a command name to its matching RespCommandsInfo + /// True if import succeeded + bool TryImportRespCommandsInfo(string path, IStreamProvider streamProvider, out IReadOnlyDictionary commandsInfo, ILogger logger = null); + + /// + /// Export RESP commands info to path using a stream provider + /// + /// Path to the file to write into + /// Stream provider to use when writing to the path + /// Dictionary that maps a command name to its matching RespCommandsInfo + /// Logger + /// True if export succeeded + bool TryExportRespCommandsInfo(string path, IStreamProvider streamProvider, IReadOnlyDictionary commandsInfo, ILogger logger = null); + } + + public class RespCommandsInfoProviderFactory + { + /// + /// Get an IRespCommandsInfoProvider instance based on its file type + /// + /// The RESP commands info file type + /// IRespCommandsInfoProvider instance + /// + public static IRespCommandsInfoProvider GetRespCommandsInfoProvider(RespCommandsObjectFileType fileType = RespCommandsObjectFileType.Default) + { + switch (fileType) + { + case RespCommandsObjectFileType.Default: + return DefaultRespCommandsInfoProvider.Instance; + default: + throw new NotImplementedException($"No RespCommandsInfoProvider exists for file type: {fileType}."); + } + } + } + + /// + /// Default commands info provider (JSON serialized array of RespCommandsInfo objects) + /// + internal class DefaultRespCommandsInfoProvider : IRespCommandsInfoProvider + { + private static readonly Lazy LazyInstance; + + public static IRespCommandsInfoProvider Instance => LazyInstance.Value; + + private static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new KeySpecConverter() } + }; + + static DefaultRespCommandsInfoProvider() + { + LazyInstance = new(() => new DefaultRespCommandsInfoProvider()); + } + + private DefaultRespCommandsInfoProvider() + { + } + + public bool TryImportRespCommandsInfo(string path, IStreamProvider streamProvider, out IReadOnlyDictionary commandsInfo, ILogger logger = null) + { + using var stream = streamProvider.Read(path); + using var streamReader = new StreamReader(stream); + + commandsInfo = default; + + try + { + var respCommands = JsonSerializer.Deserialize(streamReader.ReadToEnd(), SerializerOptions)!; + + var tmpRespCommandsInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var respCommandsInfo in respCommands) + { + tmpRespCommandsInfo.Add(respCommandsInfo.Name, respCommandsInfo); + } + + commandsInfo = new ReadOnlyDictionary(tmpRespCommandsInfo); + } + catch (JsonException je) + { + logger?.LogError(je, $"An error occurred while parsing resp commands info file (Path: {path})."); + return false; + } + + return true; + } + + public bool TryExportRespCommandsInfo(string path, IStreamProvider streamProvider, IReadOnlyDictionary commandsInfo, ILogger logger = null) + { + string jsonSettings; + + var commandsInfoToSerialize = commandsInfo.Values.OrderBy(ci => ci.Name).ToArray(); + try + { + jsonSettings = JsonSerializer.Serialize(commandsInfoToSerialize, SerializerOptions); + } + catch (NotSupportedException e) + { + logger?.LogError(e, $"An error occurred while serializing resp commands info file (Path: {path})."); + return false; + } + + var data = Encoding.ASCII.GetBytes(jsonSettings); + streamProvider.Write(path, data); + + return true; + } + } + + /// + /// Current supported RESP commands info file types + /// + public enum RespCommandsObjectFileType + { + // Default file format (JSON serialized array of RespCommandsInfo objects) + Default = 0, + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespInfo.cs b/libs/server/Resp/RespInfo.cs deleted file mode 100644 index d61563e440..0000000000 --- a/libs/server/Resp/RespInfo.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System.Collections.Generic; - -namespace Garnet.server -{ - /// - /// Info on what is supported by server - /// - public static class RespInfo - { - /// - /// Get set of RESP commands supported by Garnet server - /// - /// - public static HashSet GetCommands() - { - return new HashSet { - // Admin ops - "PING", "QUIT", "CLIENT", "SUBSCRIBE", "CONFIG", "ECHO", "INFO", "CLUSTER", "TIME", "RESET", "AUTH", "SELECT", "COMMAND", "MIGRATE", "ASKING", "LATENCY", "COMMITAOF", "FLUSHDB", "READONLY", "REPLICAOF", "MEMORY", "MONITOR", "TYPE", "MODULE", "REGISTERCS", - // Basic ops - "GET", "GETRANGE", "SET", "MGET", "MSET", "MSETNX", "SETRANGE", "GETSET", "PSETEX", "SETEX", "DEL", "UNLINK", "EXISTS", "RENAME", "EXPIRE", "PEXPIRE", "PERSIST", "TTL", "PTTL", "STRLEN", "GETDEL", "APPEND", - // Number ops - "INCR", "INCRBY", "DECR", "DECRBY", - // Checkpointing - "SAVE", "LASTSAVE", "BGSAVE", "BGREWRITEAOF", - // Sorted Set - "ZADD", "ZCARD", "ZPOPMAX", "ZSCORE", "ZREM", "ZCOUNT", "ZINCRBY", "ZRANK", "ZRANGE", "ZRANGEBYSCORE", "ZREVRANGE", "ZREVRANK", "ZREMRANGEBYLEX", "ZREMRANGEBYRANK", "ZREMRANGEBYSCORE", "ZLEXCOUNT", "ZPOPMIN", "ZRANDMEMBER", "ZDIFF", "ZSCAN", "ZMSCORE", - // List - "LPOP", "LPUSH", "RPOP", "RPUSH", "LLEN", "LTRIM", "LRANGE", "LINDEX", "LINSERT", "LREM", "RPOPLPUSH", "LMOVE", "LPUSHX", "RPUSHX", "LSET", - // Hash - "HSET", "HGET", "HMGET", "HMSET", "HDEL", "HLEN", "HEXISTS", "HGETALL", "HKEYS", "HVALS", "HINCRBY", "HINCRBYFLOAT", "HSETNX", "HRANDFIELD", "HSCAN", "HSTRLEN", - // Hyperloglog - "PFADD", "PFCOUNT", "PFMERGE", - // Bitmap - "SETBIT", "GETBIT", "BITCOUNT","BITPOS", "BITOP", "BITFIELD", - // Pub/sub - "PUBLISH", "SUBSCRIBE", "PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", - // Set - "SADD", "SREM", "SPOP", "SMEMBERS", "SCARD", "SSCAN", "SRANDMEMBER", "SISMEMBER", "SUNION", "SUNIONSTORE", "SDIFF", "SDIFFSTORE", "SMOVE", - //Scan ops - "DBSIZE", "KEYS","SCAN", - // Geospatial commands - "GEOADD", "GEOHASH", "GEODIST", "GEOPOS", "GEOSEARCH", - // Transactions: TODO - "WATCH", "UNWATCH", "MULTI", "EXEC", "DISCARD", - }; - } - } -} \ No newline at end of file diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 17bbf0b6b1..295bbffb75 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -413,6 +413,7 @@ private bool ProcessBasicCommands(RespCommand cmd, byte subcmd, int RespCommand.RUNTXP => NetworkRUNTXP(count, ptr), RespCommand.READONLY => NetworkREADONLY(), RespCommand.READWRITE => NetworkREADWRITE(), + RespCommand.COMMAND => NetworkCOMMAND(count), _ => ProcessArrayCommands(cmd, subcmd, count, ref storageApi) }; diff --git a/libs/server/Servers/RegisterApi.cs b/libs/server/Servers/RegisterApi.cs index 4b283678a1..aaf359fa4a 100644 --- a/libs/server/Servers/RegisterApi.cs +++ b/libs/server/Servers/RegisterApi.cs @@ -27,6 +27,7 @@ public RegisterApi(GarnetProvider provider) /// Numer of parameters (excluding the key, which is always the first parameter) /// Type of command (e.g., read) /// Custom functions for command logic + /// RESP command info /// /// Expiration for value, in ticks. /// -1 => remove existing expiration metadata; @@ -34,8 +35,8 @@ public RegisterApi(GarnetProvider provider) /// >0 => set expiration to given value. /// /// ID of the registered command - public int NewCommand(string name, int numParams, CommandType type, CustomRawStringFunctions customFunctions, long expirationTicks = 0) - => provider.StoreWrapper.customCommandManager.Register(name, numParams, type, customFunctions, expirationTicks); + public int NewCommand(string name, int numParams, CommandType type, CustomRawStringFunctions customFunctions, RespCommandsInfo commandInfo, long expirationTicks = 0) + => provider.StoreWrapper.customCommandManager.Register(name, numParams, type, customFunctions, commandInfo, expirationTicks); /// /// Register transaction procedure with Garnet @@ -70,9 +71,10 @@ public void NewType(int type, CustomObjectFactory factory) /// Numer of parameters (excluding the key, which is always the first parameter) /// Type of command (e.g., read) /// Type ID for factory, registered using RegisterType + /// RESP command info /// ID of the registered command - public int NewCommand(string name, int numParams, CommandType commandType, int type) - => provider.StoreWrapper.customCommandManager.Register(name, numParams, commandType, type); + public int NewCommand(string name, int numParams, CommandType commandType, int type, RespCommandsInfo commandInfo) + => provider.StoreWrapper.customCommandManager.Register(name, numParams, commandType, type, commandInfo); /// /// Register custom command with Garnet @@ -81,9 +83,10 @@ public int NewCommand(string name, int numParams, CommandType commandType, int t /// Numer of parameters (excluding the key, which is always the first parameter) /// Type of command (e.g., read) /// Custom factory for object + /// RESP command info /// ID of the registered command - public (int, int) NewCommand(string name, int numParams, CommandType commandType, CustomObjectFactory factory) - => provider.StoreWrapper.customCommandManager.Register(name, numParams, commandType, factory); + public (int, int) NewCommand(string name, int numParams, CommandType commandType, CustomObjectFactory factory, RespCommandsInfo commandInfo) + => provider.StoreWrapper.customCommandManager.Register(name, numParams, commandType, factory, commandInfo); } } \ No newline at end of file diff --git a/libs/server/Transaction/TxnRespCommands.cs b/libs/server/Transaction/TxnRespCommands.cs index 99dae8f9c3..b319a96c65 100644 --- a/libs/server/Transaction/TxnRespCommands.cs +++ b/libs/server/Transaction/TxnRespCommands.cs @@ -100,8 +100,7 @@ private bool NetworkSKIP(RespCommand cmd, byte subCommand, int count) ReadOnlySpan bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); // Retrieve the meta-data for the command to do basic sanity checking for command arguments - RespCommandsInfo commandInfo = RespCommandsInfo.findCommand(cmd, subCommand); - if (commandInfo == null) + if (!RespCommandsInfo.TryGetRespCommandInfo(cmd, out var commandInfo, subCommand, true, logger)) { while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) SendAndReset(); @@ -113,10 +112,11 @@ private bool NetworkSKIP(RespCommand cmd, byte subCommand, int count) // Check if input is valid and abort if necessary // NOTE: Negative arity means it's an expected minimum of args. Positive means exact. - bool invalidNumArgs = commandInfo.arity > 0 ? count != (commandInfo.arity) : count < -commandInfo.arity; + var arity = commandInfo.Arity > 0 ? commandInfo.Arity - 1 : commandInfo.Arity + 1; + bool invalidNumArgs = arity > 0 ? count != (arity) : count < -arity; // Watch not allowed during TXN - bool isWatch = (commandInfo.command == RespCommand.WATCH || commandInfo.command == RespCommand.WATCHMS || commandInfo.command == RespCommand.WATCHOS); + bool isWatch = (commandInfo.Command == RespCommand.WATCH || commandInfo.Command == RespCommand.WATCHMS || commandInfo.Command == RespCommand.WATCHOS); if (invalidNumArgs || isWatch) { @@ -127,7 +127,7 @@ private bool NetworkSKIP(RespCommand cmd, byte subCommand, int count) } else { - string err = string.Format(CmdStrings.GenericErrWrongNumArgs, commandInfo.nameStr); + string err = string.Format(CmdStrings.GenericErrWrongNumArgs, commandInfo.Name); while (!RespWriteUtils.WriteError(err, ref dcurr, dend)) SendAndReset(); txnManager.Abort(); diff --git a/main/GarnetServer/CustomRespCommandsInfo.json b/main/GarnetServer/CustomRespCommandsInfo.json new file mode 100644 index 0000000000..f2e9763e26 --- /dev/null +++ b/main/GarnetServer/CustomRespCommandsInfo.json @@ -0,0 +1,72 @@ +[ + { + "Command": "NONE", + "ArrayCommand": null, + "Name": "DELIFM", + "Arity": 3, + "Flags": "Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "KeySpace, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "NONE", + "ArrayCommand": null, + "Name": "MYDICTGET", + "Arity": 3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "NONE", + "ArrayCommand": null, + "Name": "MYDICTSET", + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "NONE", + "ArrayCommand": null, + "Name": "SETIFPM", + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "NONE", + "ArrayCommand": null, + "Name": "SETWPIFPGT", + "Arity": 4, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + } +] \ No newline at end of file diff --git a/main/GarnetServer/GarnetServer.csproj b/main/GarnetServer/GarnetServer.csproj index 672c07f6c9..47795e9640 100644 --- a/main/GarnetServer/GarnetServer.csproj +++ b/main/GarnetServer/GarnetServer.csproj @@ -18,6 +18,9 @@ + + PreserveNewest + PreserveNewest diff --git a/main/GarnetServer/Program.cs b/main/GarnetServer/Program.cs index fffc3684b8..02517a81b8 100644 --- a/main/GarnetServer/Program.cs +++ b/main/GarnetServer/Program.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; using System.Threading; +using Garnet.common; using Garnet.server; namespace Garnet @@ -12,6 +14,8 @@ namespace Garnet /// class Program { + private static string CustomRespCommandInfoJsonPath = "CustomRespCommandsInfo.json"; + static void Main(string[] args) { try @@ -39,19 +43,21 @@ static void Main(string[] args) /// static void RegisterExtensions(GarnetServer server) { + var customCommandsInfo = GetRespCommandsInfo(CustomRespCommandInfoJsonPath); + // Register custom command on raw strings (SETIFPM = "set if prefix match") - server.Register.NewCommand("SETIFPM", 2, CommandType.ReadModifyWrite, new SetIfPMCustomCommand()); + server.Register.NewCommand("SETIFPM", 2, CommandType.ReadModifyWrite, new SetIfPMCustomCommand(), customCommandsInfo["SETIFPM"]); // Register custom command on raw strings (SETWPIFPGT = "set with prefix, if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), customCommandsInfo["SETWPIFPGT"]); // Register custom command on raw strings (DELIFM = "delete if value matches") - server.Register.NewCommand("DELIFM", 1, CommandType.ReadModifyWrite, new DeleteIfMatchCustomCommand()); + server.Register.NewCommand("DELIFM", 1, CommandType.ReadModifyWrite, new DeleteIfMatchCustomCommand(), customCommandsInfo["DELIFM"]); // Register custom commands on objects var factory = new MyDictFactory(); - server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory); - server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory); + server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory, customCommandsInfo["MYDICTSET"]); + server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory, customCommandsInfo["MYDICTGET"]); // Register stored procedure to run a transactional command server.Register.NewTransactionProc("READWRITETX", 3, () => new ReadWriteTxn()); @@ -63,5 +69,13 @@ static void RegisterExtensions(GarnetServer server) server.Register.NewTransactionProc("SAMPLEUPDATETX", 8, () => new SampleUpdateTxn()); server.Register.NewTransactionProc("SAMPLEDELETETX", 5, () => new SampleDeleteTxn()); } + + private static IReadOnlyDictionary GetRespCommandsInfo(string path) + { + var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.Local); + var commandsInfoProvider = RespCommandsInfoProviderFactory.GetRespCommandsInfoProvider(); + commandsInfoProvider.TryImportRespCommandsInfo(path, streamProvider, out var commandsInfo); + return commandsInfo; + } } } \ No newline at end of file diff --git a/playground/ClusterStress/ClusterStress.csproj b/playground/ClusterStress/ClusterStress.csproj index ea4523144f..884bae1aab 100644 --- a/playground/ClusterStress/ClusterStress.csproj +++ b/playground/ClusterStress/ClusterStress.csproj @@ -19,7 +19,6 @@ - @@ -34,6 +33,7 @@ + diff --git a/playground/CommandInfoUpdater/CommandInfoUpdater.cs b/playground/CommandInfoUpdater/CommandInfoUpdater.cs new file mode 100644 index 0000000000..1b1cac1df4 --- /dev/null +++ b/playground/CommandInfoUpdater/CommandInfoUpdater.cs @@ -0,0 +1,468 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.ObjectModel; +using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; +using Garnet.common; +using Garnet.server; +using Microsoft.Extensions.Logging; + +namespace CommandInfoUpdater +{ + /// + /// Main logic for CommandInfoUpdater tool + /// + public class CommandInfoUpdater + { + private static readonly string GarnetCommandInfoJsonPath = "GarnetCommandsInfo.json"; + + /// + /// Tries to generate an updated JSON file containing Garnet's supported commands' info + /// + /// Output path for the updated JSON file + /// RESP server port to query commands info + /// RESP server host to query commands info + /// Commands to ignore + /// Force update all commands + /// Logger + /// True if file generated successfully + public static bool TryUpdateCommandInfo(string outputPath, int respServerPort, IPAddress respServerHost, + IEnumerable ignoreCommands, bool force, ILogger logger) + { + logger.LogInformation("Attempting to update RESP commands info..."); + + IReadOnlyDictionary existingCommandsInfo = + new Dictionary(); + if (!force && !RespCommandsInfo.TryGetRespCommandsInfo(out existingCommandsInfo, false, logger)) + { + logger.LogError($"Unable to get existing RESP commands info."); + return false; + } + + var (commandsToAdd, commandsToRemove) = + GetCommandsToAddAndRemove(existingCommandsInfo, ignoreCommands); + + if (!GetUserConfirmation(commandsToAdd, commandsToRemove, logger)) + { + logger.LogInformation($"User cancelled update operation."); + return false; + } + + if (!TryGetRespCommandsInfo(GarnetCommandInfoJsonPath, logger, out var garnetCommandsInfo) || + garnetCommandsInfo == null) + { + logger.LogError($"Unable to read Garnet RESP commands info from {GarnetCommandInfoJsonPath}."); + return false; + } + + IDictionary queriedCommandsInfo = default; + var commandsToQuery = commandsToAdd.Select(c => c.Key.Command).Except(garnetCommandsInfo.Keys).ToArray(); + if (commandsToQuery.Length > 0 && !TryGetCommandsInfo(commandsToQuery, respServerPort, respServerHost, + logger, out queriedCommandsInfo)) + { + logger.LogError("Unable to get RESP command info from local RESP server."); + return false; + } + + IDictionary additionalCommandsInfo = (queriedCommandsInfo == null + ? garnetCommandsInfo + : queriedCommandsInfo.UnionBy(garnetCommandsInfo, kvp => kvp.Key)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + var updatedCommandsInfo = GetUpdatedCommandsInfo(existingCommandsInfo, commandsToAdd, commandsToRemove, + additionalCommandsInfo); + + if (!TryWriteRespCommandsInfo(outputPath, updatedCommandsInfo, logger)) + { + logger.LogError($"Unable to write RESP commands info to path {outputPath}."); + return false; + } + + logger.LogInformation( + $"RESP commands info updated successfully! Output file written to: {Path.GetFullPath(outputPath)}"); + return true; + } + + /// + /// Try to parse JSON file containing commands info + /// + /// Path to JSON file + /// Logger + /// Dictionary mapping command name to RespCommandsInfo + /// True if deserialization was successful + private static bool TryGetRespCommandsInfo(string resourcePath, ILogger logger, out IReadOnlyDictionary commandsInfo) + { + commandsInfo = default; + + var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource, null, Assembly.GetExecutingAssembly()); + var commandsInfoProvider = RespCommandsInfoProviderFactory.GetRespCommandsInfoProvider(); + + var importSucceeded = commandsInfoProvider.TryImportRespCommandsInfo(resourcePath, + streamProvider, out var tmpCommandsInfo, logger); + + if (!importSucceeded) return false; + + commandsInfo = tmpCommandsInfo; + return true; + } + + /// + /// Compare existing commands to supported commands map to find added / removed commands / sub-commands + /// + /// Existing command names mapped to current command info + /// Commands to ignore + /// Commands to add and commands to remove mapped to a boolean determining if parent command should be added / removed + private static (IDictionary, IDictionary) + GetCommandsToAddAndRemove(IReadOnlyDictionary existingCommandsInfo, + IEnumerable ignoreCommands) + { + var commandsToAdd = new Dictionary(); + var commandsToRemove = new Dictionary(); + var commandsToIgnore = ignoreCommands != null ? new HashSet(ignoreCommands) : null; + + // Supported commands + var supportedCommands = SupportedCommand.SupportedCommandsMap; + + // Find commands / sub-commands to add + foreach (var supportedCommand in supportedCommands.Values) + { + // Ignore command if in commands to ignore + if (commandsToIgnore != null && commandsToIgnore.Contains(supportedCommand.Command)) continue; + + // If existing commands do not contain parent command, add it and indicate parent command should be added + if (!existingCommandsInfo.ContainsKey(supportedCommand.Command)) + { + commandsToAdd.Add(supportedCommand, true); + continue; + } + + // If existing commands contain parent command and no sub-commands are indicated in supported commands, no sub-commands to add + if (supportedCommand.SubCommands == null) continue; + + string[] subCommandsToAdd; + // If existing commands contain parent command and have no sub-commands, set sub-commands to add as supported command's sub-commands + if (existingCommandsInfo[supportedCommand.Command].SubCommands == null) + { + subCommandsToAdd = supportedCommand.SubCommands.ToArray(); + } + // Set sub-commands to add as the difference between existing sub-commands and supported command's sub-commands + else + { + var existingSubCommands = new HashSet(existingCommandsInfo[supportedCommand.Command] + .SubCommands + .Select(sc => sc.Name)); + subCommandsToAdd = supportedCommand.SubCommands + .Where(subCommand => !existingSubCommands.Contains(subCommand)).Select(sc => sc).ToArray(); + } + + // If there are sub-commands to add, add a new supported command with the sub-commands to add + // Indicate that parent command should not be added + if (subCommandsToAdd.Length > 0) + { + commandsToAdd.Add( + new SupportedCommand(supportedCommand.Command, supportedCommand.RespCommand, + supportedCommand.ArrayCommand, subCommandsToAdd), false); + } + } + + // Find commands / sub-commands to remove + foreach (var existingCommand in existingCommandsInfo) + { + var existingSubCommands = existingCommand.Value.SubCommands; + + // If supported commands do not contain existing parent command, add it to the list and indicate parent command should be removed + if (!supportedCommands.ContainsKey(existingCommand.Key)) + { + commandsToRemove.Add(new SupportedCommand(existingCommand.Key), true); + continue; + } + + // If supported commands contain existing parent command and no sub-commands are indicated in existing commands, no sub-commands to remove + if (existingSubCommands == null) continue; + + // Set sub-commands to remove as the difference between supported sub-commands and existing command's sub-commands + var subCommandsToRemove = (supportedCommands[existingCommand.Key].SubCommands == null + ? existingSubCommands + : existingSubCommands.Where(sc => + !supportedCommands[existingCommand.Key].SubCommands!.Contains(sc.Name))) + .Select(sc => sc.Name) + .ToArray(); + + // If there are sub-commands to remove, add a new supported command with the sub-commands to remove + // Indicate that parent command should not be removed + if (subCommandsToRemove.Length > 0) + { + commandsToRemove.Add( + new SupportedCommand(existingCommand.Key, existingCommand.Value.Command, + existingCommand.Value.ArrayCommand, subCommandsToRemove), false); + } + } + + return (commandsToAdd, commandsToRemove); + } + + /// + /// Indicates to the user which commands and sub-commands are added / removed and get their confirmation to proceed + /// + /// Commands to add + /// Commands to remove + /// Logger + /// True if user wishes to continue, false otherwise + private static bool GetUserConfirmation(IDictionary commandsToAdd, IDictionary commandsToRemove, + ILogger logger) + { + var logCommandsToAdd = commandsToAdd.Where(kvp => kvp.Value).Select(c => c.Key.Command).ToList(); + var logSubCommandsToAdd = commandsToAdd.Where(c => c.Key.SubCommands != null) + .SelectMany(c => c.Key.SubCommands!).ToList(); + var logCommandsToRemove = commandsToRemove.Where(kvp => kvp.Value).Select(c => c.Key.Command).ToList(); + var logSubCommandsToRemove = commandsToRemove.Where(c => c.Key.SubCommands != null) + .SelectMany(c => c.Key.SubCommands!).ToList(); + + logger.LogInformation( + $"Found {logCommandsToAdd.Count} commands to add and {logSubCommandsToAdd.Count} sub-commands to add."); + if (logCommandsToAdd.Count > 0) + logger.LogInformation($"Commands to add: {string.Join(", ", logCommandsToAdd)}"); + if (logSubCommandsToAdd.Count > 0) + logger.LogInformation($"Sub-Commands to add: {string.Join(", ", logSubCommandsToAdd)}"); + logger.LogInformation( + $"Found {logCommandsToRemove.Count} commands to remove and {logSubCommandsToRemove.Count} sub-commands to commandsToRemove."); + if (logCommandsToRemove.Count > 0) + logger.LogInformation($"Commands to remove: {string.Join(", ", logCommandsToRemove)}"); + if (logSubCommandsToRemove.Count > 0) + logger.LogInformation($"Sub-Commands to remove: {string.Join(", ", logSubCommandsToRemove)}"); + + if (logCommandsToAdd.Count == 0 && logSubCommandsToAdd.Count == 0 && logCommandsToRemove.Count == 0 && + logSubCommandsToRemove.Count == 0) + { + logger.LogInformation("No commands to update."); + return false; + } + + logger.LogCritical("Would you like to continue? (Y/N)"); + var inputChar = Console.ReadKey(); + while (true) + { + switch (inputChar.KeyChar) + { + case 'Y': + case 'y': + return true; + case 'N': + case 'n': + return false; + default: + logger.LogCritical("Illegal input. Would you like to continue? (Y/N)"); + inputChar = Console.ReadKey(); + break; + } + } + } + + /// + /// Query RESP server to get missing commands' info + /// + /// Command to query + /// RESP server port to query + /// RESP server host to query + /// Logger + /// Queried commands info + /// True if succeeded + private static unsafe bool TryGetCommandsInfo(string[] commandsToQuery, int respServerPort, + IPAddress respServerHost, ILogger logger, out IDictionary commandsInfo) + { + commandsInfo = default; + + // If there are no commands to query, return + if (commandsToQuery.Length == 0) return true; + + // Query the RESP server + byte[] response; + try + { + var lightClient = new LightClientRequest(respServerHost.ToString(), respServerPort, 0); + response = lightClient.SendCommand($"COMMAND INFO {string.Join(' ', commandsToQuery)}"); + } + catch (Exception e) + { + logger.LogError(e, "Encountered an error while querying local RESP server"); + return false; + } + + var tmpCommandsInfo = new Dictionary(); + + // Get a map of supported commands to Garnet's RespCommand & ArrayCommand for the parser + var supportedCommands = new ReadOnlyDictionary( + SupportedCommand.SupportedCommandsMap.ToDictionary(kvp => kvp.Key, + kvp => (kvp.Value.RespCommand, kvp.Value.ArrayCommand), StringComparer.OrdinalIgnoreCase)); + + // Parse the response + fixed (byte* respPtr = response) + { + var ptr = (byte*)Unsafe.AsPointer(ref respPtr[0]); + var end = ptr + response.Length; + + // Read the array length (# of commands info returned) + if (!RespReadUtils.ReadArrayLength(out var cmdCount, ref ptr, end)) + { + logger.LogError($"Unable to read RESP command info count from server"); + return false; + } + + // Parse each command's command info + for (var cmdIdx = 0; cmdIdx < cmdCount; cmdIdx++) + { + if (!RespCommandInfoParser.TryReadFromResp(ref ptr, end, supportedCommands, out var command) || + command == null) + { + logger.LogError( + $"Unable to read RESP command info from server for command {commandsToQuery[cmdIdx]}"); + return false; + } + + tmpCommandsInfo.Add(command.Name, command); + } + } + + commandsInfo = tmpCommandsInfo; + return true; + } + + /// + /// Update the mapping of commands info + /// + /// Existing command info mapping + /// Commands to add + /// Commands to remove + /// Queried commands info + /// + private static IReadOnlyDictionary GetUpdatedCommandsInfo( + IReadOnlyDictionary existingCommandsInfo, + IDictionary commandsToAdd, + IDictionary commandsToRemove, + IDictionary queriedCommandsInfo) + { + // Define updated commands as commands to add unified with commands to remove + var updatedCommands = + new HashSet(commandsToAdd.Keys.Union(commandsToRemove.Keys).Select(c => c.Command)); + + // Preserve command info for all commands that have not been updated + var updatedCommandsInfo = existingCommandsInfo + .Where(existingCommand => !updatedCommands.Contains(existingCommand.Key)) + .ToDictionary(existingCommand => existingCommand.Key, existingCommand => existingCommand.Value); + + // Update commands info with commands to remove - i.e. update and add commands with removed sub-commands + // Take only commands whose parent command should not be removed + foreach (var command in commandsToRemove.Where(kvp => !kvp.Value).Select(kvp => kvp.Key)) + { + // Determine updated sub-commands by subtracting from existing sub-commands + var existingSubCommands = existingCommandsInfo[command.Command].SubCommands == null + ? null + : existingCommandsInfo[command.Command].SubCommands.Select(sc => sc.Name).ToArray(); + var remainingSubCommands = existingSubCommands == null ? null : + command.SubCommands == null ? existingSubCommands : + existingSubCommands.Except(command.SubCommands).ToArray(); + + // Create updated command info based on existing command + var existingCommand = existingCommandsInfo[command.Command]; + var updatedCommand = new RespCommandsInfo + { + Command = existingCommand.Command, + ArrayCommand = existingCommand.ArrayCommand, + Name = existingCommand.Name, + Arity = existingCommand.Arity, + Flags = existingCommand.Flags, + FirstKey = existingCommand.FirstKey, + LastKey = existingCommand.LastKey, + Step = existingCommand.Step, + AclCategories = existingCommand.AclCategories, + Tips = existingCommand.Tips, + KeySpecifications = existingCommand.KeySpecifications, + SubCommands = remainingSubCommands == null || remainingSubCommands.Length == 0 + ? null + : existingCommand.SubCommands.Where(sc => remainingSubCommands.Contains(sc.Name)).ToArray() + }; + + updatedCommandsInfo.Add(updatedCommand.Name, updatedCommand); + } + + // Update commands info with commands to add + foreach (var command in commandsToAdd.Keys) + { + RespCommandsInfo baseCommand; + List updatedSubCommands; + // If parent command already exists + if (existingCommandsInfo.ContainsKey(command.Command)) + { + updatedSubCommands = existingCommandsInfo[command.Command].SubCommands == null + ? new List() + : existingCommandsInfo[command.Command].SubCommands.ToList(); + + // Add sub-commands with updated queried command info + foreach (var subCommandToAdd in command.SubCommands!) + { + updatedSubCommands.Add(queriedCommandsInfo[command.Command].SubCommands + .First(sc => sc.Name == subCommandToAdd)); + } + + // Set base command as existing sub-command + baseCommand = existingCommandsInfo[command.Command]; + } + // If parent command does not exist + else + { + // Set base command as queried command + baseCommand = queriedCommandsInfo[command.Command]; + + // Update sub-commands to contain supported sub-commands only + updatedSubCommands = command.SubCommands == null + ? null + : baseCommand.SubCommands.Where(sc => command.SubCommands.Contains(sc.Name)).ToList(); + } + + // Create updated command info based on base command & updated sub-commands + var updatedCommand = new RespCommandsInfo + { + Command = baseCommand.Command, + ArrayCommand = baseCommand.ArrayCommand, + Name = baseCommand.Name, + Arity = baseCommand.Arity, + Flags = baseCommand.Flags, + FirstKey = baseCommand.FirstKey, + LastKey = baseCommand.LastKey, + Step = baseCommand.Step, + AclCategories = baseCommand.AclCategories, + Tips = baseCommand.Tips, + KeySpecifications = baseCommand.KeySpecifications, + SubCommands = updatedSubCommands?.ToArray() + }; + + updatedCommandsInfo.Add(updatedCommand.Name, updatedCommand); + } + + return updatedCommandsInfo; + } + + /// + /// Try to serialize updated commands info to JSON file + /// + /// Output path for JSON file + /// Commands info to serialize + /// Logger + /// True if file written successfully + private static bool TryWriteRespCommandsInfo(string outputPath, + IReadOnlyDictionary commandsInfo, ILogger logger) + { + var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.Local); + var commandsInfoProvider = RespCommandsInfoProviderFactory.GetRespCommandsInfoProvider(); + + var exportSucceeded = commandsInfoProvider.TryExportRespCommandsInfo(outputPath, + streamProvider, commandsInfo, logger); + + if (!exportSucceeded) return false; + + return true; + } + } +} \ No newline at end of file diff --git a/playground/CommandInfoUpdater/CommandInfoUpdater.csproj b/playground/CommandInfoUpdater/CommandInfoUpdater.csproj new file mode 100644 index 0000000000..16129f4c48 --- /dev/null +++ b/playground/CommandInfoUpdater/CommandInfoUpdater.csproj @@ -0,0 +1,27 @@ + + + + Exe + net8.0 + enable + True + + + + + + + + + + + + + + + + + + + + diff --git a/playground/CommandInfoUpdater/GarnetCommandsInfo.json b/playground/CommandInfoUpdater/GarnetCommandsInfo.json new file mode 100644 index 0000000000..f0c80de0d5 --- /dev/null +++ b/playground/CommandInfoUpdater/GarnetCommandsInfo.json @@ -0,0 +1,162 @@ +[ + { + "Command": "COMMITAOF", + "ArrayCommand": null, + "Name": "COMMITAOF", + "Arity": -1, + "Flags": "Admin, NoScript, NoMulti, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "All", + "ArrayCommand": 39, + "Name": "COSCAN", + "Arity": -3, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Read, Slow", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "FORCEGC", + "ArrayCommand": null, + "Name": "FORCEGC", + "Arity": -1, + "Flags": "Admin, NoScript, NoMulti, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "REGISTERCS", + "ArrayCommand": null, + "Name": "REGISTERCS", + "Arity": -5, + "Flags": "Admin, NoScript, NoMulti, ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Admin, Dangerous", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "RUNTXP", + "ArrayCommand": null, + "Name": "RUNTXP", + "Arity": -2, + "Flags": "NoScript, NoMulti", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETEXNX", + "ArrayCommand": null, + "Name": "SETEXNX", + "IsInternal": true, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETEXXX", + "ArrayCommand": null, + "Name": "SETEXXX", + "IsInternal": true, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETKEEPTTL", + "ArrayCommand": null, + "Name": "SETKEEPTTL", + "IsInternal": true, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "SETKEEPTTLXX", + "ArrayCommand": null, + "Name": "SETKEEPTTLXX", + "IsInternal": true, + "Arity": -3, + "Flags": "DenyOom, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Slow, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "WATCHMS", + "ArrayCommand": null, + "Name": "WATCHMS", + "IsInternal": true, + "Arity": -3, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, + { + "Command": "WATCHOS", + "ArrayCommand": null, + "Name": "WATCHOS", + "IsInternal": true, + "Arity": -3, + "Flags": "Fast, Loading, NoScript, Stale, AllowBusy", + "FirstKey": 1, + "LastKey": -1, + "Step": 1, + "AclCategories": "Fast, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + } +] \ No newline at end of file diff --git a/playground/CommandInfoUpdater/Options.cs b/playground/CommandInfoUpdater/Options.cs new file mode 100644 index 0000000000..bb714b62b2 --- /dev/null +++ b/playground/CommandInfoUpdater/Options.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using CommandLine; + +namespace CommandInfoUpdater +{ + public class Options + { + [Option('p', "port", Required = false, Default = 6379, HelpText = "RESP server port to query")] + public int RespServerPort { get; set; } + + [Option('h', "host", Required = false, Default = "127.0.0.1", HelpText = "RESP server host to query")] + public string RespServerHost { get; set; } + + [Option('o', "output", Required = true, HelpText = "Output path for updated JSON file")] + public string OutputPath { get; set; } + + [Option('f', "force", Required = false, Default = false, HelpText = "Force overwrite existing commands info")] + public bool Force { get; set; } + + [Option('i', "ignore", Required = false, Separator = ',', HelpText = "Command names to ignore (comma separated)")] + public IEnumerable IgnoreCommands { get; set; } + } +} \ No newline at end of file diff --git a/playground/CommandInfoUpdater/Program.cs b/playground/CommandInfoUpdater/Program.cs new file mode 100644 index 0000000000..4260a5f58c --- /dev/null +++ b/playground/CommandInfoUpdater/Program.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Net; +using CommandInfoUpdater; +using CommandLine; +using CommandLine.Text; +using Microsoft.Extensions.Logging; + +/// +/// This tool helps generate an updated JSON file containing Garnet's supported commands info. +/// For this tool to run successfully, it needs to be able to query a running RESP server in order to parse its RESP command info +/// (unless you are only removing commands and/or you are adding commands that are not supported by the RESP server) +/// +/// To run this tool: +/// a) Make the desired changes to AllSupportedCommands in SupportedCommand.cs (i.e. add / remove supported commands / sub-commands) +/// b) If you're adding commands / sub-commands that are not supported by the RESP server, manually insert their command info into GarnetCommandsInfo.json. +/// c) Build and run the tool. You'll need to specify an output path and optionally the RESP server host and port (if different than default). +/// Run the tool with -h or --help for more information. +/// d) Replace Garnet's RespCommandsInfo.json file contents with the contents of the updated file. +/// e) Rebuild Garnet to include the latest changes. +/// +class Program +{ + static void Main(string[] args) + { + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh::mm::ss "; + })); + ILogger logger = loggerFactory.CreateLogger(); + + var parser = new Parser(settings => + { + settings.AutoHelp = false; + }); + + var parserResult = parser.ParseArguments(args); + + Options config = default; + + parserResult.WithParsed(op => config = op) + .WithNotParsed(errs => DisplayHelp(parserResult, errs)); + + if (config == null) return; + + if (config.RespServerPort < 0 || config.RespServerPort > ushort.MaxValue) + { + logger.LogError("Illegal value for local RESP port"); + return; + } + + if (!IPAddress.TryParse(config.RespServerHost, out var localRedisHost)) + { + logger.LogError("Unable to parse local RESP host from arguments"); + return; + } + + CommandInfoUpdater.CommandInfoUpdater.TryUpdateCommandInfo(config.OutputPath, config.RespServerPort, + localRedisHost, config.IgnoreCommands, config.Force, logger); + } + + static void DisplayHelp(ParserResult result, IEnumerable errs) + { + var helpText = HelpText.AutoBuild(result, h => + { + h.Heading = "CommandInfoUpdater - A tool for updating Garnet's supported commands info JSON"; + h.Copyright = "Copyright (c) Microsoft Corporation"; + return HelpText.DefaultParsingErrorsHandler(result, h); + }, e => e); + Console.WriteLine(helpText); + } +} \ No newline at end of file diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs new file mode 100644 index 0000000000..9b04d60c9b --- /dev/null +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Garnet.server; + +namespace CommandInfoUpdater +{ + /// + /// Defines a command supported by Garnet + /// + public class SupportedCommand + { + private static readonly SupportedCommand[] AllSupportedCommands = { + new("ACL", RespCommand.ACL, 0, new[] + { + "ACL|CAT", + "ACL|DELUSER", + "ACL|LIST", + "ACL|LOAD", + "ACL|SETUSER", + "ACL|USERS", + "ACL|WHOAMI", + }), + new("APPEND", RespCommand.APPEND), + new("ASKING", RespCommand.ASKING), + new("AUTH", RespCommand.AUTH), + new("BGSAVE", RespCommand.BGSAVE), + new("BITCOUNT", RespCommand.BITCOUNT), + new("BITFIELD", RespCommand.BITFIELD), + new("BITFIELD_RO", RespCommand.BITFIELD_RO), + new("BITOP", RespCommand.BITOP), + new("BITPOS", RespCommand.BITPOS), + new("CLIENT", RespCommand.CLIENT), + new("CLUSTER", RespCommand.CLUSTER, 0, new [] + { + "CLUSTER|ADDSLOTS", + "CLUSTER|ADDSLOTSRANGE", + "CLUSTER|BUMPEPOCH", + "CLUSTER|COUNTKEYSINSLOT", + "CLUSTER|DELSLOTS", + "CLUSTER|DELSLOTSRANGE", + "CLUSTER|FAILOVER", + "CLUSTER|FORGET", + "CLUSTER|GETKEYSINSLOT", + "CLUSTER|INFO", + "CLUSTER|KEYSLOT", + "CLUSTER|MEET", + "CLUSTER|MYID", + "CLUSTER|NODES", + "CLUSTER|REPLICAS", + "CLUSTER|REPLICATE", + "CLUSTER|RESET", + "CLUSTER|SET-CONFIG-EPOCH", + "CLUSTER|SETSLOT", + "CLUSTER|SLOTS" + + }), + new("COMMAND", RespCommand.COMMAND, 0, new [] + { + "COMMAND|INFO", + "COMMAND|COUNT", + }), + new("COMMITAOF", RespCommand.COMMITAOF), + new("CONFIG", RespCommand.CONFIG, 0, new [] + { + "CONFIG|GET", + "CONFIG|SET", + "CONFIG|REWRITE" + }), + new("COSCAN", RespCommand.All, (byte)RespCommand.COSCAN), + new("DBSIZE", RespCommand.DBSIZE), + new("DECR", RespCommand.DECR), + new("DECRBY", RespCommand.DECRBY), + new("DEL", RespCommand.DEL), + new("DISCARD", RespCommand.DISCARD), + new("ECHO", RespCommand.ECHO), + new("EXEC", RespCommand.EXEC), + new("EXISTS", RespCommand.EXISTS), + new("EXPIRE", RespCommand.EXPIRE), + new("FAILOVER", RespCommand.FAILOVER), + new("FLUSHDB", RespCommand.FLUSHDB), + new("FORCEGC", RespCommand.FORCEGC), + new("GEOADD", RespCommand.SortedSet, (byte) SortedSetOperation.GEOADD), + new("GEODIST", RespCommand.SortedSet, (byte) SortedSetOperation.GEODIST), + new("GEOHASH", RespCommand.SortedSet, (byte) SortedSetOperation.GEOHASH), + new("GEOPOS", RespCommand.SortedSet, (byte) SortedSetOperation.GEOPOS), + new("GEOSEARCH", RespCommand.SortedSet, (byte) SortedSetOperation.GEOSEARCH), + new("GET", RespCommand.GET), + new("GETBIT", RespCommand.GETBIT), + new("GETDEL", RespCommand.GETDEL), + new("GETRANGE", RespCommand.GETRANGE), + new("HDEL", RespCommand.Hash, (byte) HashOperation.HDEL), + new("HEXISTS", RespCommand.Hash, (byte) HashOperation.HEXISTS), + new("HGET", RespCommand.Hash, (byte) HashOperation.HGET), + new("HGETALL", RespCommand.Hash, (byte) HashOperation.HGETALL), + new("HINCRBY", RespCommand.Hash, (byte) HashOperation.HINCRBY), + new("HINCRBYFLOAT", RespCommand.Hash, (byte) HashOperation.HINCRBYFLOAT), + new("HKEYS", RespCommand.Hash, (byte) HashOperation.HKEYS), + new("HLEN", RespCommand.Hash, (byte) HashOperation.HLEN), + new("HMGET", RespCommand.Hash, (byte) HashOperation.HMGET), + new("HMSET", RespCommand.Hash, (byte) HashOperation.HMSET), + new("HRANDFIELD", RespCommand.Hash, (byte) HashOperation.HRANDFIELD), + new("HSCAN", RespCommand.Hash, (byte) HashOperation.HSCAN), + new("HSET", RespCommand.Hash, (byte) HashOperation.HSET), + new("HSETNX", RespCommand.Hash, (byte) HashOperation.HSETNX), + new("HSTRLEN", RespCommand.Hash, (byte) HashOperation.HSTRLEN), + new("HVALS", RespCommand.Hash, (byte) HashOperation.HVALS), + new("INCR", RespCommand.INCR), + new("INCRBY", RespCommand.INCRBY), + new("INFO", RespCommand.INFO), + new("KEYS", RespCommand.KEYS), + new("LASTSAVE", RespCommand.LASTSAVE), + new("LATENCY", RespCommand.LATENCY, 0, new [] + { + "LATENCY|HISTOGRAM", + "LATENCY|RESET" + }), + new("LINDEX", RespCommand.List, (byte) ListOperation.LINDEX), + new("LINSERT", RespCommand.List, (byte) ListOperation.LINSERT), + new("LLEN", RespCommand.List, (byte) ListOperation.LLEN), + new("LMOVE", RespCommand.List, (byte) ListOperation.LMOVE), + new("LPOP", RespCommand.List, (byte) ListOperation.LPOP), + new("LPUSH", RespCommand.List, (byte) ListOperation.LPUSH), + new("LPUSHX", RespCommand.List, (byte) ListOperation.LPUSHX), + new("LRANGE", RespCommand.List, (byte) ListOperation.LRANGE), + new("LREM", RespCommand.List, (byte) ListOperation.LREM), + new("LSET", RespCommand.List, (byte) ListOperation.LSET), + new("LTRIM", RespCommand.List, (byte) ListOperation.LTRIM), + new("MEMORY", RespCommand.MEMORY, 0, new [] + { + "MEMORY|USAGE" + }), + new("MGET", RespCommand.MGET), + new("MIGRATE", RespCommand.MIGRATE), + new("MODULE", RespCommand.MODULE), + new("MONITOR", RespCommand.MONITOR), + new("MSET", RespCommand.MSET), + new("MSETNX", RespCommand.MSETNX), + new("MULTI", RespCommand.MULTI), + new("PERSIST", RespCommand.PERSIST), + new("PEXPIRE", RespCommand.PEXPIRE), + new("PFADD", RespCommand.PFADD), + new("PFCOUNT", RespCommand.PFCOUNT), + new("PFMERGE", RespCommand.PFMERGE), + new("PING", RespCommand.PING), + new("PSETEX", RespCommand.PSETEX), + new("PSUBSCRIBE", RespCommand.PSUBSCRIBE), + new("PTTL", RespCommand.PTTL), + new("PUBLISH", RespCommand.PUBLISH), + new("PUNSUBSCRIBE", RespCommand.PUNSUBSCRIBE), + new("REGISTERCS", RespCommand.REGISTERCS), + new("QUIT", RespCommand.QUIT), + new("READONLY", RespCommand.READONLY), + new("READWRITE", RespCommand.READWRITE), + new("RENAME", RespCommand.RENAME), + new("REPLICAOF", RespCommand.REPLICAOF), + new("RESET", RespCommand.RESET), + new("RPOP", RespCommand.List, (byte) ListOperation.RPOP), + new("RPOPLPUSH", RespCommand.List, (byte) ListOperation.RPOPLPUSH), + new("RPUSH", RespCommand.List, (byte) ListOperation.RPUSH), + new("RPUSHX", RespCommand.List, (byte) ListOperation.RPUSHX), + new("RUNTXP", RespCommand.RUNTXP), + new("SADD", RespCommand.Set, (byte) SetOperation.SADD), + new("SCARD", RespCommand.Set, (byte) SetOperation.SCARD), + new("SAVE", RespCommand.SAVE), + new("SCAN", RespCommand.SCAN), + new("SDIFF", RespCommand.Set, (byte)SetOperation.SDIFF), + new("SDIFFSTORE", RespCommand.Set, (byte)SetOperation.SDIFFSTORE), + new("SELECT", RespCommand.SELECT), + new("SET", RespCommand.SET), + new("SETBIT", RespCommand.SETBIT), + new("SETEX", RespCommand.SETEX), + new("SETEXNX", RespCommand.SETEXNX), + new("SETEXXX", RespCommand.SETEXXX), + new("SETKEEPTTL", RespCommand.SETKEEPTTL), + new("SETKEEPTTLXX", RespCommand.SETKEEPTTLXX), + new("SETRANGE", RespCommand.SETRANGE), + new("SISMEMBER", RespCommand.Set, (byte) SetOperation.SISMEMBER), + new("SLAVEOF", RespCommand.SECONDARYOF), + new("SMEMBERS", RespCommand.Set, (byte) SetOperation.SMEMBERS), + new("SMOVE", RespCommand.Set, (byte) SetOperation.SMOVE), + new("SPOP", RespCommand.Set, (byte) SetOperation.SPOP), + new("SRANDMEMBER", RespCommand.Set, (byte) SetOperation.SRANDMEMBER), + new("SREM", RespCommand.Set, (byte) SetOperation.SREM), + new("SSCAN", RespCommand.Set, (byte) SetOperation.SSCAN), + new("STRLEN", RespCommand.STRLEN), + new("SUBSCRIBE", RespCommand.SUBSCRIBE), + new("SUNION", RespCommand.Set, (byte)SetOperation.SUNION), + new("SUNIONSTORE", RespCommand.Set, (byte)SetOperation.SUNIONSTORE), + new("TIME", RespCommand.TIME), + new("TTL", RespCommand.TTL), + new("TYPE", RespCommand.TYPE), + new("UNLINK", RespCommand.UNLINK), + new("UNSUBSCRIBE", RespCommand.UNSUBSCRIBE), + new("UNWATCH", RespCommand.UNWATCH), + new("WATCH", RespCommand.WATCH), + new("WATCHMS", RespCommand.WATCHMS), + new("WATCHOS", RespCommand.WATCHOS), + new("ZADD", RespCommand.SortedSet, (byte) SortedSetOperation.ZADD), + new("ZCARD", RespCommand.SortedSet, (byte) SortedSetOperation.ZCARD), + new("ZCOUNT", RespCommand.SortedSet, (byte) SortedSetOperation.ZCOUNT), + new("ZDIFF", RespCommand.SortedSet, (byte) SortedSetOperation.ZDIFF), + new("ZINCRBY", RespCommand.SortedSet, (byte) SortedSetOperation.ZINCRBY), + new("ZLEXCOUNT", RespCommand.SortedSet, (byte) SortedSetOperation.ZLEXCOUNT), + new("ZMSCORE", RespCommand.SortedSet, (byte) SortedSetOperation.ZMSCORE), + new("ZPOPMAX", RespCommand.SortedSet, (byte) SortedSetOperation.ZPOPMAX), + new("ZPOPMIN", RespCommand.SortedSet, (byte) SortedSetOperation.ZPOPMIN), + new("ZRANDMEMBER", RespCommand.SortedSet, (byte) SortedSetOperation.ZRANDMEMBER), + new("ZRANGE", RespCommand.SortedSet, (byte) SortedSetOperation.ZRANGE), + new("ZRANGEBYSCORE", RespCommand.SortedSet, (byte) SortedSetOperation.ZRANGEBYSCORE), + new("ZRANK", RespCommand.SortedSet, (byte) SortedSetOperation.ZRANK), + new("ZREM", RespCommand.SortedSet, (byte) SortedSetOperation.ZREM), + new("ZREMRANGEBYLEX", RespCommand.SortedSet, (byte) SortedSetOperation.ZREMRANGEBYLEX), + new("ZREMRANGEBYRANK", RespCommand.SortedSet, (byte) SortedSetOperation.ZREMRANGEBYRANK), + new("ZREMRANGEBYSCORE", RespCommand.SortedSet, (byte) SortedSetOperation.ZREMRANGEBYSCORE), + new("ZREVRANGE", RespCommand.SortedSet, (byte) SortedSetOperation.ZREVRANGE), + new("ZREVRANK", RespCommand.SortedSet, (byte) SortedSetOperation.ZREVRANK), + new("ZSCAN", RespCommand.SortedSet, (byte) SortedSetOperation.ZSCAN), + new("ZSCORE", RespCommand.SortedSet, (byte) SortedSetOperation.ZSCORE), + }; + + private static readonly Lazy> LazySupportedCommandsMap = + new(() => + { + return AllSupportedCommands.ToDictionary(sc => sc.Command, sc => sc); + }); + + /// + /// Map between a supported command's name and its SupportedCommand object + /// + public static IReadOnlyDictionary SupportedCommandsMap => LazySupportedCommandsMap.Value; + + /// + /// Supported command's name + /// + public string Command { get; set; } + + /// + /// Supported command's sub-commands' names + /// + public HashSet SubCommands { get; set; } + + /// + /// Garnet RespCommand + /// + public RespCommand RespCommand { get; set; } + + /// + /// Garnet ArrayCommand + /// + public byte? ArrayCommand { get; set; } + + /// + /// Default constructor provided for JSON serialization + /// + public SupportedCommand() + { + + } + + /// + /// SupportedCommand constructor + /// + /// Supported command name + /// RESP Command enum + /// Array Command byte (if applicable) + /// List of supported sub-command names (optional) + public SupportedCommand(string command, RespCommand respCommand = RespCommand.NONE, byte? arrayCommand = null, + IEnumerable subCommands = null) : this() + { + Command = command; + SubCommands = subCommands == null ? null : new HashSet(subCommands); + RespCommand = respCommand; + ArrayCommand = arrayCommand; + } + } +} \ No newline at end of file diff --git a/samples/MetricsMonitor/Configuration.cs b/samples/MetricsMonitor/Configuration.cs index 4aacc4cfec..d74a1519ff 100644 --- a/samples/MetricsMonitor/Configuration.cs +++ b/samples/MetricsMonitor/Configuration.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using Garnet.server; using StackExchange.Redis; @@ -17,10 +18,14 @@ public class Configuration { public static ConfigurationOptions GetConfig(string address, int port = default, bool allowAdmin = false, bool useTLS = false, string tlsHost = null) { + var commands = RespCommandsInfo.TryGetRespCommandNames(out var names) + ? new HashSet(names) + : new HashSet(); + var configOptions = new ConfigurationOptions { EndPoints = { { address, port }, }, - CommandMap = CommandMap.Create(RespInfo.GetCommands()), + CommandMap = CommandMap.Create(commands), ConnectTimeout = 100_000, SyncTimeout = 100_000, AllowAdmin = allowAdmin, diff --git a/test/Garnet.test.cluster/ClusterRedirectTests.cs b/test/Garnet.test.cluster/ClusterRedirectTests.cs index 9f883ff1ca..b6aa3a2a93 100644 --- a/test/Garnet.test.cluster/ClusterRedirectTests.cs +++ b/test/Garnet.test.cluster/ClusterRedirectTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Garnet.common; using Microsoft.Extensions.Logging; using NUnit.Framework; using NUnit.Framework.Internal; diff --git a/test/Garnet.test.cluster/Garnet.test.cluster.csproj b/test/Garnet.test.cluster/Garnet.test.cluster.csproj index 7f5fca593f..c6c6c0a290 100644 --- a/test/Garnet.test.cluster/Garnet.test.cluster.csproj +++ b/test/Garnet.test.cluster/Garnet.test.cluster.csproj @@ -10,7 +10,6 @@ 1701;1702;1591 - diff --git a/test/Garnet.test/Garnet.test.csproj b/test/Garnet.test/Garnet.test.csproj index 22ca67312f..04360f88fb 100644 --- a/test/Garnet.test/Garnet.test.csproj +++ b/test/Garnet.test/Garnet.test.csproj @@ -21,8 +21,15 @@ + + + PreserveNewest + + + - + + PreserveNewest diff --git a/test/Garnet.test/GarnetServerConfigTests.cs b/test/Garnet.test/GarnetServerConfigTests.cs index 7cc8d70540..6fd0ab405a 100644 --- a/test/Garnet.test/GarnetServerConfigTests.cs +++ b/test/Garnet.test/GarnetServerConfigTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using CommandLine; +using Garnet.common; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using NUnit.Framework; @@ -32,7 +33,7 @@ public void TearDown() public void DefaultConfigurationOptionsCoverage() { string json; - var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource); + var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource, null, Assembly.GetExecutingAssembly()); using (var stream = streamProvider.Read(ServerSettingsManager.DefaultOptionsEmbeddedFileName)) { using (var streamReader = new StreamReader(stream)) diff --git a/test/Garnet.test/RespAofTests.cs b/test/Garnet.test/RespAofTests.cs index a52bd1d08d..6e4d566f81 100644 --- a/test/Garnet.test/RespAofTests.cs +++ b/test/Garnet.test/RespAofTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using Garnet.server; @@ -14,6 +15,8 @@ namespace Garnet.test public class RespAofTests { GarnetServer server; + private IReadOnlyDictionary respCustomCommandsInfo; + static readonly SortedSetEntry[] entries = [ new SortedSetEntry("a", 1), @@ -32,6 +35,8 @@ public class RespAofTests public void Setup() { TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + Assert.IsTrue(TestUtils.TryGetCustomCommandsInfo(out respCustomCommandsInfo)); + Assert.IsNotNull(respCustomCommandsInfo); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, enableAOF: true, lowMemory: true); server.Start(); } @@ -414,8 +419,8 @@ public void AofUpsertCustomObjectRecoverTest() void RegisterCustomCommand(GarnetServer gServer) { var factory = new MyDictFactory(); - gServer.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory); - gServer.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory); + gServer.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory, respCustomCommandsInfo["MYDICTSET"]); + gServer.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory, respCustomCommandsInfo["MYDICTGET"]); } server.Dispose(false); diff --git a/test/Garnet.test/RespCommandTests.cs b/test/Garnet.test/RespCommandTests.cs new file mode 100644 index 0000000000..50f4cd0d99 --- /dev/null +++ b/test/Garnet.test/RespCommandTests.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Garnet.server; +using NUnit.Framework; +using StackExchange.Redis; +using SetOperation = Garnet.server.SetOperation; + +namespace Garnet.test +{ + /// + /// This test class tests the RESP COMMAND and COMMAND INFO commands + /// + [TestFixture] + public class RespCommandTests + { + GarnetServer server; + private IReadOnlyDictionary respCommandsInfo; + private IReadOnlyDictionary respCustomCommandsInfo; + + [SetUp] + public void Setup() + { + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + Assert.IsTrue(RespCommandsInfo.TryGetRespCommandsInfo(out respCommandsInfo)); + Assert.IsTrue(TestUtils.TryGetCustomCommandsInfo(out respCustomCommandsInfo)); + Assert.IsNotNull(respCommandsInfo); + Assert.IsNotNull(respCustomCommandsInfo); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, disablePubSub: true); + server.Start(); + } + + [TearDown] + public void TearDown() + { + server.Dispose(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir); + } + + /// + /// Verify that all existing combinations of RespCommand and subcommand byte (if relevant) + /// have a matching RespCommandInfo objects defined in RespCommandsInfo + /// + [Test] + public void CommandsInfoCoverageTest() + { + // Get all command-subcommand combinations that have RespCommandInfo objects defined + var existingCombinations = new Dictionary>(); + foreach (var commandInfo in respCommandsInfo.Values) + { + if (!existingCombinations.ContainsKey(commandInfo.Command)) + existingCombinations.Add(commandInfo.Command, new HashSet()); + if (commandInfo.ArrayCommand.HasValue) + existingCombinations[commandInfo.Command].Add(commandInfo.ArrayCommand.Value); + } + + // RespCommands that can be ignored + var ignoreCommands = new HashSet() + { + RespCommand.NONE, + RespCommand.COSCAN, + RespCommand.CustomCmd, + RespCommand.CustomObjCmd, + RespCommand.CustomTxn, + RespCommand.INVALID, + }; + + var missingCombinations = new List<(RespCommand, byte)>(); + foreach (var respCommand in Enum.GetValues()) + { + if (ignoreCommands.Contains(respCommand)) continue; + + var arrayCommandEnumType = (respCommand) switch + { + RespCommand.Set => typeof(SetOperation), + RespCommand.Hash => typeof(HashOperation), + RespCommand.List => typeof(ListOperation), + RespCommand.SortedSet => typeof(SortedSetOperation), + _ => default + }; + + if (arrayCommandEnumType != default) + { + foreach (var arrayCommand in Enum.GetValues(arrayCommandEnumType)) + { + if (!existingCombinations.ContainsKey(respCommand) || + !existingCombinations[respCommand].Contains((byte)arrayCommand)) + { + missingCombinations.Add((respCommand, (byte)arrayCommand)); + } + } + } + else if (respCommand == RespCommand.All) + { + if (!existingCombinations.ContainsKey(respCommand) || + !existingCombinations[respCommand].Contains((byte)RespCommand.COSCAN)) + { + missingCombinations.Add((respCommand, (byte)RespCommand.COSCAN)); + } + } + else + { + if (!existingCombinations.ContainsKey(respCommand)) + missingCombinations.Add((respCommand, 0)); + } + } + + // Verify that there are no missing combinations + Assert.IsEmpty(missingCombinations); + } + + /// + /// Test COMMAND command + /// + [Test] + public void CommandTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Get all commands using COMMAND command + var results = (RedisResult[])db.Execute("COMMAND"); + + Assert.IsNotNull(results); + Assert.AreEqual(respCommandsInfo.Count, results.Length); + + // Register custom commands + var customCommandsRegistered = RegisterCustomCommands(); + + // Get all commands (including custom commands) using COMMAND command + results = (RedisResult[])db.Execute("COMMAND"); + + Assert.IsNotNull(results); + Assert.AreEqual(respCommandsInfo.Count + customCommandsRegistered, results.Length); + } + + /// + /// Test COMMAND INFO command + /// + [Test] + public void CommandInfoTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Get all commands using COMMAND INFO command + var results = (RedisResult[])db.Execute("COMMAND", "INFO"); + + Assert.IsNotNull(results); + Assert.AreEqual(respCommandsInfo.Count, results.Length); + + // Register custom commands + var customCommandsRegistered = RegisterCustomCommands(); + + // Get all commands (including custom commands) using COMMAND INFO command + results = (RedisResult[])db.Execute("COMMAND", "INFO"); + + Assert.IsNotNull(results); + Assert.AreEqual(respCommandsInfo.Count + customCommandsRegistered, results.Length); + } + + /// + /// Test COMMAND COUNT command + /// + [Test] + public void CommandCountTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Get command count + var commandCount = (int)db.Execute("COMMAND", "COUNT"); + + Assert.AreEqual(respCommandsInfo.Count, commandCount); + + // Register custom commands + var customCommandsRegistered = RegisterCustomCommands(); + + // Get command count (including custom commands) + commandCount = (int)db.Execute("COMMAND", "COUNT"); + + Assert.AreEqual(respCommandsInfo.Count + customCommandsRegistered, commandCount); + } + + /// + /// Test COMMAND DOCS command + /// This is not yet implemented, yet it should return an empty array + /// so to not crash clients that use this command at initialization + /// + [Test] + public void CommandDocsTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Get all commands using COMMAND INFO command + var results = (RedisResult[])db.Execute("COMMAND", "DOCS"); + + Assert.IsNotNull(results); + Assert.IsEmpty(results); + } + + /// + /// Test COMMAND with unknown subcommand + /// + [Test] + public void CommandUnknownSubcommandTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var unknownSubCommand = "UNKNOWN"; + + // Get all commands using COMMAND INFO command + try + { + db.Execute("COMMAND", unknownSubCommand); + Assert.Fail(); + } + catch (RedisServerException e) + { + var expectedErrorMessage = string.Format(CmdStrings.GenericErrUnknownSubCommand, unknownSubCommand, RespCommand.COMMAND); + Assert.AreEqual(expectedErrorMessage, e.Message); + } + } + + /// + /// Test COMMAND INFO [command-name [command-name ...]] + /// + [Test] + public void CommandInfoWithCommandNamesTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Get basic commands using COMMAND INFO command + var results = (RedisResult[])db.Execute("COMMAND", "INFO", "GET", "SET"); + + Assert.IsNotNull(results); + Assert.AreEqual(2, results.Length); + + var getInfo = (RedisResult[])results[0]; + VerifyCommandInfo("GET", getInfo); + + var setInfo = (RedisResult[])results[1]; + VerifyCommandInfo("SET", setInfo); + } + + private int RegisterCustomCommands() + { + var factory = new MyDictFactory(); + server.Register.NewCommand("SETIFPM", 2, CommandType.ReadModifyWrite, new SetIfPMCustomCommand(), respCustomCommandsInfo["SETIFPM"]); + server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory, respCustomCommandsInfo["MYDICTSET"]); + server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory, respCustomCommandsInfo["MYDICTGET"]); + + return 3; + } + + private void VerifyCommandInfo(string cmdName, RedisResult[] result) + { + Assert.IsTrue(respCommandsInfo.ContainsKey(cmdName)); + var cmdInfo = respCommandsInfo[cmdName]; + + Assert.IsNotNull(result); + Assert.AreEqual(10, result.Length); + Assert.AreEqual(cmdInfo.Name, (string)result[0]); + Assert.AreEqual(cmdInfo.Arity, (int)result[1]); + } + } +} \ No newline at end of file diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index c0457f0e37..9784091960 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -23,6 +23,7 @@ namespace Garnet.test public class RespCustomCommandTests { GarnetServer server; + private IReadOnlyDictionary respCustomCommandsInfo; private string _extTestDir1; private string _extTestDir2; @@ -32,6 +33,9 @@ public void Setup() _extTestDir1 = Path.Combine(TestUtils.MethodTestDir, "test1"); _extTestDir2 = Path.Combine(TestUtils.MethodTestDir, "test2"); + Assert.IsTrue(TestUtils.TryGetCustomCommandsInfo(out respCustomCommandsInfo)); + Assert.IsNotNull(respCustomCommandsInfo); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, disablePubSub: true, @@ -52,7 +56,7 @@ public void TearDown() public void CustomCommandTest1() { // Register sample custom command (SETIFPM = "set if prefix match") - int x = server.Register.NewCommand("SETIFPM", 2, CommandType.ReadModifyWrite, new SetIfPMCustomCommand()); + int x = server.Register.NewCommand("SETIFPM", 2, CommandType.ReadModifyWrite, new SetIfPMCustomCommand(), respCustomCommandsInfo["SETIFPM"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -97,7 +101,8 @@ public void CustomCommandTest1() public void CustomCommandTest2() { // Register custom command on raw strings (SETWPIFPGT = "set with prefix, if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -177,7 +182,8 @@ public void CustomCommandTest2() public void CustomCommandTest3() { // Register custom command on raw strings (SETWPIFPGT = "set with prefix, if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -207,7 +213,8 @@ public void CustomCommandTest3() public void CustomCommandTest4() { // Register custom command on raw strings (SETWPIFPGT = "set with prefix, if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -237,7 +244,8 @@ public void CustomCommandTest4() public void CustomCommandTest5() { // Register custom command on raw strings (SETWPIFPGT = "set with prefix, if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -276,7 +284,7 @@ public void CustomCommandTest5() public void CustomCommandTest6() { // Register sample custom command (SETIFPM = "set if prefix match") - server.Register.NewCommand("DELIFM", 1, CommandType.ReadModifyWrite, new DeleteIfMatchCustomCommand()); + server.Register.NewCommand("DELIFM", 1, CommandType.ReadModifyWrite, new DeleteIfMatchCustomCommand(), respCustomCommandsInfo["DELIFM"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -306,8 +314,8 @@ public void CustomObjectCommandTest1() { // Register sample custom command on object var factory = new MyDictFactory(); - server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory); - server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory); + server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory, respCustomCommandsInfo["MYDICTSET"]); + server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory, respCustomCommandsInfo["MYDICTGET"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -343,7 +351,8 @@ public void CustomObjectCommandTest1() public void CustomCommandSetWhileKeyHasTtlTest() { // Register sample custom command (SETWPIFPGT = "set if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -376,7 +385,8 @@ public void CustomCommandSetWhileKeyHasTtlTest() public void CustomCommandSetAfterKeyDeletedWithTtlTest() { // Register sample custom command (SETWPIFPGT = "set if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -407,8 +417,8 @@ public void CustomObjectCommandTest2() { // Register sample custom command on object var factory = new MyDictFactory(); - server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory); - server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory); + server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory, respCustomCommandsInfo["MYDICTSET"]); + server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory, respCustomCommandsInfo["MYDICTGET"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -444,7 +454,8 @@ public void CustomObjectCommandTest2() public async Task CustomCommandSetFollowedByTtlTestAsync() { // Register sample custom command (SETWPIFPGT = "set if prefix greater than") - server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand()); + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -473,7 +484,8 @@ public async Task CustomCommandSetFollowedByTtlTestAsync() public async Task CustomCommandSetWithCustomExpirationTestAsync() { // Register sample custom command (SETWPIFPGT = "set if prefix greater than") - server.Register.NewCommand("SETWPIFPGTE", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + server.Register.NewCommand("SETWPIFPGT", 2, CommandType.ReadModifyWrite, new SetWPIFPGTCustomCommand(), + respCustomCommandsInfo["SETWPIFPGT"], expirationTicks: TimeSpan.FromSeconds(4).Ticks); // provide default expiration at registration time using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); @@ -483,14 +495,14 @@ public async Task CustomCommandSetWithCustomExpirationTestAsync() string origValue = "foovalue0"; long prefix = 0; - await db.ExecuteAsync("SETWPIFPGTE", key, origValue, BitConverter.GetBytes(prefix)); + await db.ExecuteAsync("SETWPIFPGT", key, origValue, BitConverter.GetBytes(prefix)); string retValue = db.StringGet(key); Assert.AreEqual(origValue, retValue.Substring(8)); string newValue1 = "foovalue10"; prefix = 1; - await db.ExecuteAsync("SETWPIFPGTE", key, newValue1, BitConverter.GetBytes(prefix)); + await db.ExecuteAsync("SETWPIFPGT", key, newValue1, BitConverter.GetBytes(prefix)); retValue = db.StringGet(key); Assert.AreEqual(newValue1, retValue.Substring(8)); diff --git a/test/Garnet.test/RespScanCommandsTests.cs b/test/Garnet.test/RespScanCommandsTests.cs index cb73cf6541..417595e7f5 100644 --- a/test/Garnet.test/RespScanCommandsTests.cs +++ b/test/Garnet.test/RespScanCommandsTests.cs @@ -15,11 +15,14 @@ namespace Garnet.test public class RespScanCommandsTests { GarnetServer server; + private IReadOnlyDictionary respCustomCommandsInfo; [SetUp] public void Setup() { TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + Assert.IsTrue(TestUtils.TryGetCustomCommandsInfo(out respCustomCommandsInfo)); + Assert.IsNotNull(respCustomCommandsInfo); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir); server.Start(); } @@ -549,8 +552,8 @@ public void CustomObjectScanCommandTest() // create a custom object var factory = new MyDictFactory(); - server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory); - server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory); + server.Register.NewCommand("MYDICTSET", 2, CommandType.ReadModifyWrite, factory, respCustomCommandsInfo["MYDICTSET"]); + server.Register.NewCommand("MYDICTGET", 1, CommandType.Read, factory, respCustomCommandsInfo["MYDICTGET"]); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index 54e2368486..3f16df2d20 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Garnet.common; using Garnet.server; using NUnit.Framework; using StackExchange.Redis; diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 7b04ed288a..ae3ccf0469 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -9,6 +9,7 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Security; +using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading; using Garnet.client; @@ -41,6 +42,12 @@ internal static class TestUtils /// static readonly bool useTestLogger = false; + private static int procId = Process.GetCurrentProcess().Id; + private static string CustomRespCommandInfoJsonPath = "CustomRespCommandsInfo.json"; + + private static bool CustomCommandsInfoInitialized; + private static IReadOnlyDictionary RespCustomCommandsInfo; + internal static string AzureTestContainer { get @@ -69,6 +76,48 @@ internal static bool IsRunningAzureTests } } + /// + /// Get command info for custom commands defined in custom commands json file + /// + /// Mapping between command name and command info + /// Logger + /// + internal static bool TryGetCustomCommandsInfo(out IReadOnlyDictionary customCommandsInfo, ILogger logger = null) + { + customCommandsInfo = default; + + if (!CustomCommandsInfoInitialized && !TryInitializeCustomCommandsInfo(logger)) return false; + + customCommandsInfo = RespCustomCommandsInfo; + return true; + } + + private static bool TryInitializeCustomCommandsInfo(ILogger logger) + { + if (!TryGetRespCommandsInfo(CustomRespCommandInfoJsonPath, logger, out var tmpCustomCommandsInfo)) + return false; + + RespCustomCommandsInfo = tmpCustomCommandsInfo; + CustomCommandsInfoInitialized = true; + return true; + } + + private static bool TryGetRespCommandsInfo(string resourcePath, ILogger logger, out IReadOnlyDictionary commandsInfo) + { + commandsInfo = default; + + var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource, null, Assembly.GetExecutingAssembly()); + var commandsInfoProvider = RespCommandsInfoProviderFactory.GetRespCommandsInfoProvider(); + + var importSucceeded = commandsInfoProvider.TryImportRespCommandsInfo(resourcePath, + streamProvider, out var tmpCommandsInfo, logger); + + if (!importSucceeded) return false; + + commandsInfo = tmpCommandsInfo; + return true; + } + static bool IsAzuriteRunning() { // If Azurite is running, it will run on localhost and listen on port 10000 and/or 10001. @@ -465,7 +514,10 @@ public static ConfigurationOptions GetConfig( string authPassword = null, X509CertificateCollection certificates = null) { - var cmds = RespInfo.GetCommands(); + var cmds = RespCommandsInfo.TryGetRespCommandNames(out var names) + ? new HashSet(names) + : new HashSet(); + if (disablePubSub) { cmds.Remove("SUBSCRIBE"); diff --git a/website/docs/commands/analytics.md b/website/docs/commands/analytics.md index 19b82d5392..7ab10a7c4d 100644 --- a/website/docs/commands/analytics.md +++ b/website/docs/commands/analytics.md @@ -42,6 +42,21 @@ Similarly the command handles increments and decrements of the specified integer Integer Reply: the bit value stored at offset. +--- +### BITFIELD_RO + +#### Syntax + +```bash +BITFIELD_RO key [GET encoding offset [GET encoding offset ...]] +``` + +Read-only variant of the [BITFIELD](#bitfield) command. It is like the original [BITFIELD](#bitfield) but only accepts GET subcommand and can safely be used in read-only replicas. + +#### Resp Reply + +Array reply: each entry being the corresponding result of the sub-command given at the same position. + --- ### BITOP AND diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index 404861d54e..d14f0b42c5 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -47,7 +47,7 @@ Note that this list is subject to change as we continue to expand our API comman | | [WHOAMI](acl.md#acl-whoami) | ➕ | | | **BITMAP** | [BITCOUNT](analytics.md#bitcount) | ➕ | | | | [BITFIELD](analytics.md#bitfield) | ➕ | | -| | BITFIELD_RO | ➖ | | +| | [BITFIELD_RO](analytics.md#bitfield_ro) | ➕ | | | | [BITOP AND](analytics.md#bitop-and) | ➕ | | | | [BITOP NOT](analytics.md#bitop-not) | ➕ | | | | [BITPOS](analytics.md#bitpos) | ➕ | | @@ -191,6 +191,12 @@ Note that this list is subject to change as we continue to expand our API comman | | BGREWRITEAOF | ➖ | | | | [BGSAVE](checkpoint.md#bgsave) | ➕ | | | | [COMMAND](server.md#command) | ➕ | | +| | [COMMAND COUNT](server.md#command-count) | ➕ | | +| | COMMAND DOCS | ➖ | | +| | COMMAND GETKEYS | ➖ | | +| | COMMAND GETKEYSANDFLAGS | ➖ | | +| | [COMMAND INFO](server.md#command-info) | ➕ | | +| | COMMAND LIST | ➖ | | | | [COMMITAOF](server.md#commitaof) | ➕ | | | | [CONFIG GET](server.md#config-get) | ➕ | | | | [CONFIG SET](server.md#config-set) | ➕ | | diff --git a/website/docs/commands/server.md b/website/docs/commands/server.md index 38d4674308..cbb98bbf06 100644 --- a/website/docs/commands/server.md +++ b/website/docs/commands/server.md @@ -13,11 +13,43 @@ slug: server COMMAND ``` -Return an array with details about every Redis command. +Return an array with details about every Garnet command. #### Resp Reply -Array reply: a nested list of command details. The order of the commands in the array is random. +Array reply: a nested list of command details. + +--- +### COMMAND COUNT +#### Syntax + +```bash +COMMAND COUNT +``` + +Returns Integer reply of number of total commands in this Garnet server. + +#### Resp Reply + +Integer reply: the number of commands returned by COMMAND. + +--- +### COMMAND INFO +#### Syntax + +```bash +COMMAND INFO [command-name [command-name ...]] +``` + +Returns Array reply of details about multiple Garnet commands. + +Same result format as COMMAND except you can specify which commands get returned. + +If you request details about non-existing commands, their return position will be nil. + +#### Resp Reply + +Array reply: a nested list of command details. --- ### COMMITAOF diff --git a/website/docs/dev/garnet-api.md b/website/docs/dev/garnet-api.md index 0f057c815a..e82ef0131f 100644 --- a/website/docs/dev/garnet-api.md +++ b/website/docs/dev/garnet-api.md @@ -4,41 +4,45 @@ sidebar_label: Garnet API title: Garnet API --- -The IGarnetAPI interface contains the operators exposed to the public API, which ultimately perform operations over the keys stored in Garnet. It inherits from IGarnetReadApi and IGarnetAdvancedApi. +The **IGarnetApi** interface contains the operators exposed to the public API, which ultimately perform operations over the keys stored in Garnet. It inherits from **IGarnetReadApi** (read-only commands interface) and **IGarnetAdvancedApi** (advanced API calls). -For adding an new operator or command to the API, you should add a new method to the IGarnetApi, if this is a write operation, or to the IGarnetReadApi in the case of a read one. +For adding an new operator or command to the API, add a new method signature to the **IGarnetReadApi** interface in case the command performs read-only operations, or **IGarnetApi** otherwise. -### Adding a new command +### Adding a new command to Garnet -A new command is implemented with the following methods: (See ZADD for further details) +_If you are trying to add a command for your specific Garnet server instance, see [Custom Commands](custom-commands.md)_ -* A private unsafe bool Network[CommandName] in the internal class RespServerSession. +To add a new command to Garnet, follow these steps: -**Example** +1. If your command operates on an **object** (i.e. List, SortedSet etc.), add a new enum value to the ```[ObjectName]Operation``` enum in ```Garnet.server/Objects/[ObjectName]/[ObjectName]Object.cs```\ +Otherwise, add a new enum value to the ```RespCommand``` enum in ```Garnet.server/Resp/RespCommand.cs```. +2. Add the parsing logic of the new command name to ```Garnet.server/Resp/RespCommand.cs```. If the command has a **fixed number of arguments**, add parsing logic to the **FastParseCommand** method. Otherwise, add parsing logic to the **FastParseArrayCommand** method. +3. Add a new method signature to **IGarnetReadApi**, in case the command performs read-only operations, or to **IGarnetApi** otherwise (```Garnet.server/API/IGarnetAPI.cs```). +4. Add a new method to the **RespServerSession** class. This method will parse the command from the network buffer, call the storage layer API (method declared in step #3) and write the RESP formatted response back to the network buffer (note that the **RespServerSession** class is divided across several .cs files, object-specific commands will reside under ```Garnet.server/Resp/Objects/[ObjectName]Commands.cs```, while others will reside under ```Garnet.server/Resp/[Admin|Array|Basic|etc...]Commands.cs```, depending on the command type). +5. Back in ```Garnet.server/Resp/RespCommand.cs```, add the new command case to the **ProcessBasicCommands** or **ProcessArrayCommands** method respectively, calling the method that was added in step #4. +6. Add a new method to the **StorageSession** class. This method is part of the storage layer. This storage API ONLY performs the RMW or Read operation calls, and it wraps the Tsavorite API. (note that the **StorageSession** class is divided across several .cs files, **object store** operations will reside under ```Garnet.server/Storage/Session/ObjectStore/[ObjectName]Ops.cs```, while **main store** operations will mainly reside under ```Garnet.server/Storage/Session/MainStore/MainStoreOps.cs```, with some exceptions).\ +To implement the storage-level logic of the new command, follow these guidelines according to the new command type: + * ***Single-key object store command***: If you are adding a command that operates on a single object, the implementation of this method will simply be a call ```[Read|RMW]ObjectStoreOperation[WithOutput]```, which in turn will call the ```Operate``` method in ```Garnet.server/Objects/[ObjectName]/[ObjectName]Object.cs```, where you will have to add a new case for the command and and the object-specific command implementation in ```Garnet.server/Objects/[ObjectName]/[ObjectName]ObjectImpl.cs``` + * ***Multi-key object store command***: If you are adding a command that operates on multiple objects, you may need to create a transaction in which you will appropriately lock the keys (using the ```TransactionManager``` instance). You can then operate on multiple objects (for instance using the ```GET``` & ```SET``` operations). + * ***Main-store command***: If you are adding a command that operates on the main store, you'll need to call Tsavorite's ```Read``` or ```RMW``` methods. If you are calling ```RMW```, you will need to implement the initialization and in-place / copy update functionality of the new command in ```Garnet.server/Storage/Functions/MainStore/RMWMethods.cs```. +7. If the command supports being called in a transaction context, add a new case for the command in the ```TransactionManager.GetKeys``` method and return the appropriate key locks required by the command (```Garnet.server/Transaction/TxnKeyManager.cs```). +8. Add tests that run the command and check its output under valid and invalid conditions, test using both ```SE.Redis``` and ```LightClient```, if applicable. For object commands, add tests to ```Garnet.test/Resp[ObjectName]Tests.cs```. For other commands, add to ```Garnet.test/RespTests.cs``` or ```Garnet.test/Resp[AdminCommands|etc...]Tests.cs```, depending on the command type. +9. Add newly supported command documentation to the appropriate markdown file under ```website/docs/commands/```, and specify the command as supported in ```website/docs/commands/api-compatibility.md```. +10. Add command info by following the next [section](#adding-supported-command-info) -```csharp -private unsafe bool NetworkZADD(int count, byte* ptr, ref TObjectContext objectStoreContext) -``` +:::tip +Before you start implementing your command logic, add a basic test that calls the new command, it will be easier to debug and implement missing logic as you go along. +::: -This method contains the operations that read and parse the message from the network buffer and write the resp formatted response from the command, back to the network buffer. - -* An internal method Try[CommandName] in the class StorageSession. - -**Example** - -```csharp -internal GarnetStatus Try[CommandName](ref byte[] key, ref SpanByte input, ref FasterObjectOutput output, ref TObjectContext objectStoreContext) -``` - -This method is part of the Storage layer. This storage API ONLY performs the RMW or Read operation calls, and it wraps over the Tsavorite API. All the current implemented commands can be found under the folder *Storage*, inside the Garnet.Server project (folder */libs/server/Storage/Session/)*. - -* An internal method Try[CommandName] using the ArgSlice type. - -```csharp -internal GarnetStatus Try[CommandName](ArgSlice key, ArgSlice score, ArgSlice member, out int result, ref TObjectContext objectStoreContext) -``` - -This method is optional. Notice this ArgSlice type is used instead of byte[], also this method does not read any value from the network buffer and it does not rely on the RESP protocol. It is the way to extended or add functionality to the API, that can be used in server procedures that use csharp code. +### Adding command info +Each supported RESP command in Garnet should have an entry in ```Garnet.server/Resp/RespCommandsInfo.json```, specifying the command's info.\ +A command's info can be added manually, but we recommend using the ```CommandInfoUpdater``` tool to update the JSON file (can be found under ```playground/```). +The ```CommandInfoUpdater``` tool calculates the difference between existing commands in ```Garnet.server/Resp/RespCommandsInfo.json``` and commands specified in ```CommandInfoUpdater/SupportedCommands.cs```. It then attempts to add / remove commands' info as necessary.\ +Info for Garnet-only commands is retrieved from ```CommandInfoUpdater/GarnetCommandsInfo.json```, and info for other RESP commands is retrieved from an external RESP server (which you will need to run locally / have access to in order to run this tool). +To add command info to Garnet, follow these steps: +1. Add the supported command and its supported sub-commands (if applicable) to ```CommandInfoUpdater/SupportedCommands.cs```. +2. If you are adding a Garnet-specific command, add its info to ```CommandInfoUpdater/GarnetCommandsInfo.json```. +3. Build & run the tool (for syntax help run the tool with `-h` or `--help`). \ No newline at end of file