From ea748efeecd06fa166380dbbdef89ba1bf8dff42 Mon Sep 17 00:00:00 2001 From: Badrish Chandramouli Date: Tue, 8 Oct 2024 18:08:39 -0700 Subject: [PATCH 01/15] CrossSlot support for Lua EVAL/EVALSHA (#708) --- libs/server/Resp/Parser/RespCommand.cs | 38 +++++++++---------- libs/server/Resp/RespCommandsInfo.cs | 4 +- .../Resp/RespServerSessionSlotVerify.cs | 2 +- .../Garnet.test.cluster/ClusterTestContext.cs | 6 ++- .../RedirectTests/BaseCommand.cs | 28 ++++++++++++++ .../ClusterSlotVerificationTests.cs | 5 ++- test/Garnet.test/TestUtils.cs | 8 +++- 7 files changed, 64 insertions(+), 27 deletions(-) diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 1773d7a3e2..cc212fa549 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -18,7 +18,7 @@ public enum RespCommand : byte { NONE = 0x00, - // Read-only commands + // Read-only commands. NOTE: Should immediately follow NONE. BITCOUNT, BITFIELD_RO, BITPOS, @@ -78,10 +78,10 @@ public enum RespCommand : byte ZREVRANGEBYSCORE, ZREVRANK, ZSCAN, - ZSCORE, // Note: Update OneIfRead if adding new read commands after this + ZSCORE, // Note: Last read command should immediately precede FirstWriteCommand // Write commands - APPEND, // Note: Update OneIfWrite if adding new write commands before this + APPEND, // Note: Update FirstWriteCommand if adding new write commands before this BITFIELD, DECR, DECRBY, @@ -157,7 +157,11 @@ public enum RespCommand : byte BITOP_AND, BITOP_OR, BITOP_XOR, - BITOP_NOT, // Note: Update OneIfWrite if adding new write commands after this + BITOP_NOT, // Note: Update LastWriteCommand if adding new write commands after this + + // Script execution commands + EVAL, + EVALSHA, // Note: Update LastDataCommand if adding new data commands after this // Neither read nor write key commands ASYNC, @@ -211,9 +215,7 @@ public enum RespCommand : byte CustomObjCmd, CustomProcedure, - // Scripting commands - EVAL, - EVALSHA, + // Script commands SCRIPT, ACL, @@ -418,21 +420,19 @@ public static ReadOnlySpan ExpandForACLs(this RespCommand cmd) }; } - public static RespCommand FirstReadCommand() - => RespCommand.NONE + 1; + internal const RespCommand FirstReadCommand = RespCommand.NONE + 1; + + internal const RespCommand LastReadCommand = RespCommand.APPEND - 1; - public static RespCommand LastReadCommand() - => RespCommand.APPEND - 1; + internal const RespCommand FirstWriteCommand = RespCommand.APPEND; - public static RespCommand FirstWriteCommand() - => RespCommand.APPEND; + internal const RespCommand LastWriteCommand = RespCommand.BITOP_NOT; - public static RespCommand LastWriteCommand() - => RespCommand.BITOP_NOT; + internal const RespCommand LastDataCommand = RespCommand.EVALSHA; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsReadOnly(this RespCommand cmd) - => cmd <= LastReadCommand(); + => cmd <= LastReadCommand; public static bool IsDataCommand(this RespCommand cmd) { @@ -446,7 +446,7 @@ public static bool IsDataCommand(this RespCommand cmd) RespCommand.FLUSHALL => false, RespCommand.KEYS => false, RespCommand.SCAN => false, - _ => cmd >= FirstReadCommand() && cmd <= LastWriteCommand() + _ => cmd >= FirstReadCommand && cmd <= LastDataCommand }; } @@ -454,10 +454,10 @@ public static bool IsDataCommand(this RespCommand cmd) public static bool IsWriteOnly(this RespCommand cmd) { // If cmd < RespCommand.Append - underflows, setting high bits - var test = (uint)((int)cmd - (int)FirstWriteCommand()); + var test = (uint)((int)cmd - (int)FirstWriteCommand); // Force to be branchless for same reasons as OneIfRead - return test <= (LastWriteCommand() - FirstWriteCommand()); + return test <= (LastWriteCommand - FirstWriteCommand); } /// diff --git a/libs/server/Resp/RespCommandsInfo.cs b/libs/server/Resp/RespCommandsInfo.cs index ea66f4bf26..a2ddfbf147 100644 --- a/libs/server/Resp/RespCommandsInfo.cs +++ b/libs/server/Resp/RespCommandsInfo.cs @@ -200,8 +200,8 @@ private static bool TryInitializeRespCommandsInfo(ILogger logger = null) ) ); - FastBasicRespCommandsInfo = new RespCommandsInfo[(int)RespCommandExtensions.LastWriteCommand() - (int)RespCommandExtensions.FirstReadCommand()]; - for (var i = (int)RespCommandExtensions.FirstReadCommand(); i < (int)RespCommandExtensions.LastWriteCommand(); i++) + FastBasicRespCommandsInfo = new RespCommandsInfo[(int)RespCommandExtensions.LastDataCommand - (int)RespCommandExtensions.FirstReadCommand]; + for (var i = (int)RespCommandExtensions.FirstReadCommand; i < (int)RespCommandExtensions.LastDataCommand; i++) { FlattenedRespCommandsInfo.TryGetValue((RespCommand)i, out var commandInfo); FastBasicRespCommandsInfo[i - 1] = commandInfo; diff --git a/libs/server/Resp/RespServerSessionSlotVerify.cs b/libs/server/Resp/RespServerSessionSlotVerify.cs index 44b7f60d43..a69202864c 100644 --- a/libs/server/Resp/RespServerSessionSlotVerify.cs +++ b/libs/server/Resp/RespServerSessionSlotVerify.cs @@ -67,7 +67,7 @@ bool CanServeSlot(RespCommand cmd) case FindKeysKeyNum: var findKeysKeyNum = (FindKeysKeyNum)specs[0].FindKeys; csvi.firstKey = searchIndex.Index + findKeysKeyNum.FirstKey - 1; - csvi.lastKey = searchIndex.Index + parseState.GetInt(0); + csvi.lastKey = csvi.firstKey + parseState.GetInt(searchIndex.Index + findKeysKeyNum.KeyNumIdx - 1); csvi.step = findKeysKeyNum.KeyStep; break; case FindKeysUnknown: diff --git a/test/Garnet.test.cluster/ClusterTestContext.cs b/test/Garnet.test.cluster/ClusterTestContext.cs index 12afd1ec8a..172d64e536 100644 --- a/test/Garnet.test.cluster/ClusterTestContext.cs +++ b/test/Garnet.test.cluster/ClusterTestContext.cs @@ -111,7 +111,8 @@ public void CreateInstances( ServerCredential clusterCreds = new ServerCredential(), AadAuthenticationSettings authenticationSettings = null, bool disablePubSub = true, - int metricsSamplingFrequency = 0) + int metricsSamplingFrequency = 0, + bool enableLua = false) { endpoints = TestUtils.GetEndPoints(shards, 7000); nodes = TestUtils.CreateGarnetCluster( @@ -142,7 +143,8 @@ public void CreateInstances( authPassword: clusterCreds.password, certificates: certificates, authenticationSettings: authenticationSettings, - metricsSamplingFrequency: metricsSamplingFrequency); + metricsSamplingFrequency: metricsSamplingFrequency, + enableLua: enableLua); foreach (var node in nodes) node.Start(); diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs index 76e79c86ac..256235481e 100644 --- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs +++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs @@ -889,4 +889,32 @@ public override ArraySegment[] SetupSingleSlotRequest() } } #endregion + + #region LuaCommands + internal class EVAL : BaseCommand + { + public override bool IsArrayCommand => true; + public override bool ArrayResponse => false; + public override string Command => nameof(EVAL); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return ["return 'OK'", "3", ssk[0], ssk[1], ssk[2]]; + } + + public override string[] GetCrossSlotRequest() + { + var csk = GetCrossSlotKeys; + return ["return 'OK'", "3", csk[0], csk[1], csk[2]]; + } + + public override ArraySegment[] SetupSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + var setup = new ArraySegment[] { new ArraySegment(["EVAL", "return 'OK'", "3", ssk[1], ssk[2], ssk[3]]) }; + return setup; + } + } + #endregion } \ No newline at end of file diff --git a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs index b999122522..63649c2360 100644 --- a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs +++ b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs @@ -60,6 +60,7 @@ public class ClusterSlotVerificationTests new SINTERSTORE(), new SINTER(), new LMOVE(), + new EVAL(), }; @@ -109,7 +110,7 @@ public void OneTimeSetUp() context = new ClusterTestContext(); context.Setup([]); - context.CreateInstances(3); + context.CreateInstances(3, enableLua: true); context.CreateConnection(); // Assign all slots to node 0 @@ -241,6 +242,7 @@ void GarnetClientSessionClusterDown(BaseCommand command) [TestCase("SINTERSTORE")] [TestCase("SINTER")] [TestCase("LMOVE")] + [TestCase("EVAL")] public void ClusterOKTest(string commandName) { var requestNodeIndex = sourceIndex; @@ -330,6 +332,7 @@ void GarnetClientSessionOK(BaseCommand command) [TestCase("SINTERSTORE")] [TestCase("SINTER")] [TestCase("LMOVE")] + [TestCase("EVAL")] public void ClusterCROSSSLOTTest(string commandName) { var requestNodeIndex = sourceIndex; diff --git a/test/Garnet.test/TestUtils.cs b/test/Garnet.test/TestUtils.cs index 139794b9d4..07cb9c3def 100644 --- a/test/Garnet.test/TestUtils.cs +++ b/test/Garnet.test/TestUtils.cs @@ -358,7 +358,8 @@ public static GarnetServer[] CreateGarnetCluster( X509CertificateCollection certificates = null, ILoggerFactory loggerFactory = null, AadAuthenticationSettings authenticationSettings = null, - int metricsSamplingFrequency = 0) + int metricsSamplingFrequency = 0, + bool enableLua = false) { if (UseAzureStorage) IgnoreIfNotRunningAzureTests(); @@ -398,7 +399,8 @@ public static GarnetServer[] CreateGarnetCluster( certificates: certificates, logger: loggerFactory?.CreateLogger("GarnetServer"), aadAuthenticationSettings: authenticationSettings, - metricsSamplingFrequency: metricsSamplingFrequency); + metricsSamplingFrequency: metricsSamplingFrequency, + enableLua: enableLua); ClassicAssert.IsNotNull(opts); int iter = 0; @@ -444,6 +446,7 @@ public static GarnetServerOptions GetGarnetServerOptions( X509CertificateCollection certificates = null, AadAuthenticationSettings aadAuthenticationSettings = null, int metricsSamplingFrequency = 0, + bool enableLua = false, ILogger logger = null) { if (UseAzureStorage) @@ -528,6 +531,7 @@ public static GarnetServerOptions GetGarnetServerOptions( AuthSettings = useAcl ? authenticationSettings : (authPassword != null ? authenticationSettings : null), ClusterUsername = authUsername, ClusterPassword = authPassword, + EnableLua = enableLua, }; if (lowMemory) From 622abe78ab86f968c33543d4e9974a5a4ce80e76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:40:46 -0600 Subject: [PATCH 02/15] Bump System.Text.Json from 8.0.4 to 8.0.5 (#711) * Bump System.Text.Json from 8.0.4 to 8.0.5 Bumps [System.Text.Json](https://github.com/dotnet/runtime) from 8.0.4 to 8.0.5. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v8.0.4...v8.0.5) --- updated-dependencies: - dependency-name: System.Text.Json dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Bumping System.Text.Json verion in nuspec file --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tal Zaccai --- Directory.Packages.props | 2 +- Garnet.nuspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index df66545fc2..ad93e54cd0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + diff --git a/Garnet.nuspec b/Garnet.nuspec index 199256eea4..b4ab8f4eda 100644 --- a/Garnet.nuspec +++ b/Garnet.nuspec @@ -27,7 +27,7 @@ - + README.md From 45e704bcfdff68f5cff7f6451bcc6958fd2ba9cb Mon Sep 17 00:00:00 2001 From: Vasileios Zois <96085550+vazois@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:05:32 -0700 Subject: [PATCH 03/15] Cleanup Slot Verification (#709) * refactor slot verification logic under new folder * add comments to redirect tests * refactor list slot verification * refactor slot verification for set operations * generalize FindNumKeys * refactor slot verification for GEOADD commands * refactor slot verification for SortedSet commands * refactor slot verification for Hash commands * remove unused methods from cluster slot verification * add more comments * address comments --- .../ClusterSlotVerificationResult.cs | 0 .../ClusterSlotVerify.cs | 0 .../RespClusterSlotVerify.cs} | 27 - libs/server/Cluster/IClusterSession.cs | 5 - libs/server/Resp/Objects/HashCommands.cs | 55 - libs/server/Resp/Objects/ListCommands.cs | 74 -- libs/server/Resp/Objects/SetCommands.cs | 35 - .../Resp/Objects/SharedObjectCommands.cs | 6 - libs/server/Resp/Objects/SortedSetCommands.cs | 67 -- .../Resp/Objects/SortedSetGeoCommands.cs | 10 - .../Resp/RespServerSessionSlotVerify.cs | 10 - .../server/Transaction/TxnClusterSlotCheck.cs | 6 +- libs/server/Transaction/TxnKeyManager.cs | 2 +- .../RedirectTests/BaseCommand.cs | 982 +++++++++++++++++- .../ClusterSlotVerificationTests.cs | 338 +++++- .../RedirectTests/CustomProcedure.cs | 37 + 16 files changed, 1352 insertions(+), 302 deletions(-) rename libs/cluster/Session/{ => SlotVerification}/ClusterSlotVerificationResult.cs (100%) rename libs/cluster/Session/{ => SlotVerification}/ClusterSlotVerify.cs (100%) rename libs/cluster/Session/{ClusterSlotCheck.cs => SlotVerification/RespClusterSlotVerify.cs} (74%) create mode 100644 test/Garnet.test.cluster/RedirectTests/CustomProcedure.cs diff --git a/libs/cluster/Session/ClusterSlotVerificationResult.cs b/libs/cluster/Session/SlotVerification/ClusterSlotVerificationResult.cs similarity index 100% rename from libs/cluster/Session/ClusterSlotVerificationResult.cs rename to libs/cluster/Session/SlotVerification/ClusterSlotVerificationResult.cs diff --git a/libs/cluster/Session/ClusterSlotVerify.cs b/libs/cluster/Session/SlotVerification/ClusterSlotVerify.cs similarity index 100% rename from libs/cluster/Session/ClusterSlotVerify.cs rename to libs/cluster/Session/SlotVerification/ClusterSlotVerify.cs diff --git a/libs/cluster/Session/ClusterSlotCheck.cs b/libs/cluster/Session/SlotVerification/RespClusterSlotVerify.cs similarity index 74% rename from libs/cluster/Session/ClusterSlotCheck.cs rename to libs/cluster/Session/SlotVerification/RespClusterSlotVerify.cs index 6e91f5b856..1248b08e24 100644 --- a/libs/cluster/Session/ClusterSlotCheck.cs +++ b/libs/cluster/Session/SlotVerification/RespClusterSlotVerify.cs @@ -65,33 +65,6 @@ private void WriteClusterSlotVerificationMessage(ClusterConfig config, ClusterSl SendAndReset(ref dcurr, ref dend); } - /// - /// Check if read or read/write is permitted on a single key and generate the appropriate response - /// LOCAL | ~LOCAL | MIGRATING EXISTS | MIGRATING ~EXISTS | IMPORTING ASKING | IMPORTING ~ASKING - /// R OK | -MOVED | OK | -ASK | OK | -MOVED - /// R/W OK | -MOVED | -MIGRATING | -ASK | OK | -MOVED - /// - /// True if redirect, False if can serve - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool NetworkSingleKeySlotVerify(ReadOnlySpan key, bool readOnly, byte SessionAsking, ref byte* dcurr, ref byte* dend) - { - fixed (byte* keyPtr = key) - { - var keySlice = new ArgSlice(keyPtr, key.Length); - // If cluster is not enabled or a transaction is running skip slot check - if (!clusterProvider.serverOptions.EnableCluster || txnManager.state == TxnState.Running) return false; - - var config = clusterProvider.clusterManager.CurrentConfig; - var vres = SingleKeySlotVerify(ref config, ref keySlice, readOnly, SessionAsking); - - if (vres.state == SlotVerifiedState.OK) - return false; - else - WriteClusterSlotVerificationMessage(config, vres, ref dcurr, ref dend); - return true; - } - } - /// /// Check if read/write is permitted on an array of keys and generate appropriate resp response. /// diff --git a/libs/server/Cluster/IClusterSession.cs b/libs/server/Cluster/IClusterSession.cs index 2d80b13070..dcf58aef4a 100644 --- a/libs/server/Cluster/IClusterSession.cs +++ b/libs/server/Cluster/IClusterSession.cs @@ -56,11 +56,6 @@ public interface IClusterSession /// unsafe bool CheckSingleKeySlotVerify(ArgSlice keySlice, bool readOnly, byte SessionAsking); - /// - /// Single key slot verify (write result to network) - /// - unsafe bool NetworkSingleKeySlotVerify(ReadOnlySpan key, bool readOnly, byte SessionAsking, ref byte* dcurr, ref byte* dend); - /// /// Key array slot verify (write result to network) /// diff --git a/libs/server/Resp/Objects/HashCommands.cs b/libs/server/Resp/Objects/HashCommands.cs index 59a606e92a..cc09f7279a 100644 --- a/libs/server/Resp/Objects/HashCommands.cs +++ b/libs/server/Resp/Objects/HashCommands.cs @@ -34,11 +34,6 @@ private unsafe bool HashSet(RespCommand command, ref TGarnetApi stor var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var hop = command switch { @@ -101,11 +96,6 @@ private bool HashGet(RespCommand command, ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -158,11 +148,6 @@ private bool HashGetAll(RespCommand command, ref TGarnetApi storageA var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -213,11 +198,6 @@ private bool HashGetMultiple(RespCommand command, ref TGarnetApi sto var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -270,11 +250,6 @@ private bool HashRandomField(RespCommand command, ref TGarnetApi sto var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var paramCount = 1; var withValues = false; var includedCount = false; @@ -373,11 +348,6 @@ private unsafe bool HashLength(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -427,11 +397,6 @@ private unsafe bool HashStrLength(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -483,11 +448,6 @@ private unsafe bool HashDelete(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -537,11 +497,6 @@ private unsafe bool HashExists(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -594,11 +549,6 @@ private unsafe bool HashKeys(RespCommand command, ref TGarnetApi sto var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var op = command switch { @@ -663,11 +613,6 @@ private unsafe bool HashIncrement(RespCommand command, ref TGarnetAp var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var op = command switch { diff --git a/libs/server/Resp/Objects/ListCommands.cs b/libs/server/Resp/Objects/ListCommands.cs index 159b00feec..876a9ae034 100644 --- a/libs/server/Resp/Objects/ListCommands.cs +++ b/libs/server/Resp/Objects/ListCommands.cs @@ -29,11 +29,6 @@ private unsafe bool ListPush(RespCommand command, ref TGarnetApi sto var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var lop = command switch { @@ -106,11 +101,6 @@ private unsafe bool ListPop(RespCommand command, ref TGarnetApi stor } } - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var lop = command switch { @@ -176,11 +166,6 @@ private unsafe bool ListPosition(ref TGarnetApi storageApi) var element = parseState.GetArgSliceByRef(1).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -373,19 +358,8 @@ private unsafe bool ListBlockingMove(RespCommand command) var srcKey = parseState.GetArgSliceByRef(0); - if (NetworkSingleKeySlotVerify(srcKey.ReadOnlySpan, false)) - { - return true; - } - // Read destination key cmdArgs[0] = parseState.GetArgSliceByRef(1); - - if (NetworkSingleKeySlotVerify(cmdArgs[0].ReadOnlySpan, false)) - { - return true; - } - var srcDir = parseState.GetArgSliceByRef(2); var dstDir = parseState.GetArgSliceByRef(3); @@ -447,11 +421,6 @@ private bool ListLength(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -512,11 +481,6 @@ private bool ListTrim(ref TGarnetApi storageApi) return true; } - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -576,11 +540,6 @@ private bool ListRange(ref TGarnetApi storageApi) return true; } - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -643,11 +602,6 @@ private bool ListIndex(ref TGarnetApi storageApi) return true; } - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -711,11 +665,6 @@ private bool ListInsert(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -780,11 +729,6 @@ private bool ListRemove(ref TGarnetApi storageApi) return true; } - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -823,7 +767,6 @@ private bool ListRemove(ref TGarnetApi storageApi) return true; } - /// /// LMOVE source destination [LEFT | RIGHT] [LEFT | RIGHT] /// @@ -841,12 +784,6 @@ private bool ListMove(ref TGarnetApi storageApi) var srcKey = parseState.GetArgSliceByRef(0); var dstKey = parseState.GetArgSliceByRef(1); - if (NetworkSingleKeySlotVerify(srcKey.ReadOnlySpan, false) || - NetworkSingleKeySlotVerify(dstKey.ReadOnlySpan, false)) - { - return true; - } - var srcDirSlice = parseState.GetArgSliceByRef(2); var dstDirSlice = parseState.GetArgSliceByRef(3); @@ -902,12 +839,6 @@ private bool ListRightPopLeftPush(ref TGarnetApi storageApi) var srcKey = parseState.GetArgSliceByRef(0); var dstKey = parseState.GetArgSliceByRef(1); - if (NetworkSingleKeySlotVerify(srcKey.ReadOnlySpan, false) || - NetworkSingleKeySlotVerify(dstKey.ReadOnlySpan, false)) - { - return true; - } - if (!ListMove(srcKey, dstKey, OperationDirection.Right, OperationDirection.Left, out var node, ref storageApi, out var garnetStatus)) return false; @@ -980,11 +911,6 @@ public bool ListSet(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { diff --git a/libs/server/Resp/Objects/SetCommands.cs b/libs/server/Resp/Objects/SetCommands.cs index de85bf141b..c4126a1360 100644 --- a/libs/server/Resp/Objects/SetCommands.cs +++ b/libs/server/Resp/Objects/SetCommands.cs @@ -32,11 +32,6 @@ private unsafe bool SetAdd(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -271,11 +266,6 @@ private unsafe bool SetRemove(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -328,11 +318,6 @@ private unsafe bool SetLength(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -383,11 +368,6 @@ private unsafe bool SetMembers(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -434,11 +414,6 @@ private unsafe bool SetIsMember(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -493,11 +468,6 @@ private unsafe bool SetPop(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var countParameter = int.MinValue; if (parseState.Count == 2) { @@ -621,11 +591,6 @@ private unsafe bool SetRandomMember(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var countParameter = int.MinValue; if (parseState.Count == 2) { diff --git a/libs/server/Resp/Objects/SharedObjectCommands.cs b/libs/server/Resp/Objects/SharedObjectCommands.cs index 9583f98355..c41c10be7c 100644 --- a/libs/server/Resp/Objects/SharedObjectCommands.cs +++ b/libs/server/Resp/Objects/SharedObjectCommands.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; using Garnet.common; using Tsavorite.core; @@ -47,11 +46,6 @@ private unsafe bool ObjectScan(GarnetObjectType objectType, ref TGar return true; } - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var input = new ObjectInput { header = new RespInputHeader diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index fdb15b79ab..550fcd4daf 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -31,11 +31,6 @@ private unsafe bool SortedSetAdd(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var input = new ObjectInput { header = new RespInputHeader @@ -84,11 +79,6 @@ private unsafe bool SortedSetRemove(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var input = new ObjectInput { header = new RespInputHeader @@ -138,11 +128,6 @@ private unsafe bool SortedSetLength(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var input = new ObjectInput { header = new RespInputHeader @@ -195,11 +180,6 @@ private unsafe bool SortedSetRange(RespCommand command, ref TGarnetA var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var op = command switch { @@ -264,11 +244,6 @@ private unsafe bool SortedSetScore(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -324,11 +299,6 @@ private unsafe bool SortedSetScores(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -384,11 +354,6 @@ private unsafe bool SortedSetPop(RespCommand command, ref TGarnetApi var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var popCount = 1; if (parseState.Count == 2) @@ -463,11 +428,6 @@ private unsafe bool SortedSetCount(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -526,11 +486,6 @@ private unsafe bool SortedSetLengthByValue(RespCommand command, ref var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, command != RespCommand.ZREMRANGEBYLEX)) - { - return true; - } - var op = command switch { @@ -604,11 +559,6 @@ private unsafe bool SortedSetIncrement(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -660,12 +610,6 @@ private unsafe bool SortedSetRank(RespCommand command, ref TGarnetAp // Get the key for SortedSet var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var includeWithScore = false; // Read WITHSCORE @@ -749,11 +693,6 @@ private unsafe bool SortedSetRemoveRange(RespCommand command, ref TG var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - var op = command switch { @@ -813,12 +752,6 @@ private unsafe bool SortedSetRandomMember(ref TGarnetApi storageApi) // Get the key for the Sorted Set var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var paramCount = 1; var includeWithScores = false; var includedCount = false; diff --git a/libs/server/Resp/Objects/SortedSetGeoCommands.cs b/libs/server/Resp/Objects/SortedSetGeoCommands.cs index bbf261b0c3..18eeef1d5b 100644 --- a/libs/server/Resp/Objects/SortedSetGeoCommands.cs +++ b/libs/server/Resp/Objects/SortedSetGeoCommands.cs @@ -29,11 +29,6 @@ private unsafe bool GeoAdd(ref TGarnetApi storageApi) var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, false)) - { - return true; - } - // Prepare input var input = new ObjectInput { @@ -105,11 +100,6 @@ private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - if (NetworkSingleKeySlotVerify(keyBytes, true)) - { - return true; - } - var op = command switch { diff --git a/libs/server/Resp/RespServerSessionSlotVerify.cs b/libs/server/Resp/RespServerSessionSlotVerify.cs index a69202864c..42651624f4 100644 --- a/libs/server/Resp/RespServerSessionSlotVerify.cs +++ b/libs/server/Resp/RespServerSessionSlotVerify.cs @@ -12,16 +12,6 @@ namespace Garnet.server /// internal sealed unsafe partial class RespServerSession : ServerSessionBase { - /// - /// This method is used to verify slot ownership for provided key. - /// On error this method writes to response buffer but does not drain recv buffer (caller is responsible for draining). - /// - /// Key bytes - /// Whether caller is going to perform a readonly or read/write operation. - /// True when ownership is verified, false otherwise - bool NetworkSingleKeySlotVerify(ReadOnlySpan key, bool readOnly) - => clusterSession != null && clusterSession.NetworkSingleKeySlotVerify(key, readOnly, SessionAsking, ref dcurr, ref dend); - /// /// This method is used to verify slot ownership for provided array of key argslices. /// diff --git a/libs/server/Transaction/TxnClusterSlotCheck.cs b/libs/server/Transaction/TxnClusterSlotCheck.cs index 37ad8d55b7..da587fc774 100644 --- a/libs/server/Transaction/TxnClusterSlotCheck.cs +++ b/libs/server/Transaction/TxnClusterSlotCheck.cs @@ -7,7 +7,7 @@ namespace Garnet.server { sealed unsafe partial class TransactionManager { - //Keys involved in the current transaction + // Keys involved in the current transaction ArgSlice[] keys; int keyCount; @@ -20,7 +20,7 @@ sealed unsafe partial class TransactionManager /// public void SaveKeyArgSlice(ArgSlice argSlice) { - //Execute method only if clusterEnabled + // Execute method only if clusterEnabled if (!clusterEnabled) return; // Grow the buffer if needed if (keyCount >= keys.Length) @@ -39,7 +39,7 @@ public void SaveKeyArgSlice(ArgSlice argSlice) /// public unsafe void UpdateRecvBufferPtr(byte* recvBufferPtr) { - //Execute method only if clusterEnabled + // Execute method only if clusterEnabled if (!clusterEnabled) return; if (recvBufferPtr != saveKeyRecvBufferPtr) { diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index 0dec851736..ea0df21b08 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -303,7 +303,7 @@ private int ListKeys(bool isObject, LockType type) if (!NumUtils.TryParse(numKeysArg.ReadOnlySpan, out int numKeys)) return -2; - for (int i = 0; i < numKeys; i++) + for (var i = 0; i < numKeys; i++) { var key = respSession.GetCommandAsArgSlice(out success); if (!success) return -2; diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs index 256235481e..e4ef2e8fbc 100644 --- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs +++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs @@ -26,9 +26,27 @@ public abstract class BaseCommand { public static ReadOnlySpan HashTag => "{1234}"u8; + /// + /// Indicates if command is a multi-key operation + /// public abstract bool IsArrayCommand { get; } + + /// + /// Indicates if command response is a single value or an array of values. + /// NOTE: used only for GarnetClientSession that requires to differentiate between string and string[] + /// public abstract bool ArrayResponse { get; } + + /// + /// Command requires an existing key to be inserted before the command executes + /// Example: RENAME,LSET + /// NOTE: The example command throw an error if key is not set which is not relevant when testing OK operation. + /// public virtual bool RequiresExistingKey => false; + + /// + /// Command name + /// public abstract string Command { get; } public BaseCommand() @@ -37,15 +55,48 @@ public BaseCommand() GetCrossSlotKeys = crossSlotKeys(); } + /// + /// Get slot value for keys from + /// public int GetSlot => HashSlotUtils.HashSlot(Encoding.ASCII.GetBytes(GetSingleSlotKeys[0])); + /// + /// Get a list of keys that are guaranteed to hash to same slot + /// public List GetSingleSlotKeys { get; } + + /// + /// Get a list of keys where at least one hashes to the same slot + /// public List GetCrossSlotKeys { get; } + /// + /// Generate a request for this command that references a single slot. + /// NOTE: available for both single and multi-key operations + /// + /// public abstract string[] GetSingleSlotRequest(); + + /// + /// Generate a request for this command that references at least two slots + /// NOTE: available only for multi-key operations + /// + /// public abstract string[] GetCrossSlotRequest(); + + /// + /// Setup for a given command that references a single slot + /// + /// public abstract ArraySegment[] SetupSingleSlotRequest(); + /// + /// Generate a list of keys that hash to a single slot + /// + /// + /// + /// + /// private List singleSlotKeys(int klen = 16, int kcount = 32, int kEndTag = 4) { var ssk = new List(); @@ -62,6 +113,12 @@ private List singleSlotKeys(int klen = 16, int kcount = 32, int kEndTag return ssk; } + /// + /// Generate a list of keys that hash to multi slots + /// + /// + /// + /// private List crossSlotKeys(int klen = 16, int kcount = 32) { var csk = new List(); @@ -74,6 +131,9 @@ private List crossSlotKeys(int klen = 16, int kcount = 32) return csk; } + /// + /// Get command with parameters containing keys that hash to a single slot + /// public string[] GetSingleSlotRequestWithCommand { get @@ -87,6 +147,10 @@ public string[] GetSingleSlotRequestWithCommand return args; } } + + /// + /// Get command with parameters containing keys that hash to at least two slots + /// public string[] GetCrossslotRequestWithCommand { get @@ -104,8 +168,11 @@ public string[] GetCrossslotRequestWithCommand public class DummyCommand : BaseCommand { + /// public override bool IsArrayCommand => false; + /// public override bool ArrayResponse => false; + /// public override string Command => commandName; readonly string commandName; @@ -114,10 +181,13 @@ public DummyCommand(string commandName) this.commandName = commandName; } + /// public override string[] GetSingleSlotRequest() => throw new NotImplementedException(); + /// public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + /// public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); } @@ -857,6 +927,125 @@ public override ArraySegment[] SetupSingleSlotRequest() return setup; } } + + internal class SADD : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(SADD); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "a", "b", "c"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class SREM : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(SREM); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "a", "b", "c"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class SCARD : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(SCARD); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class SMEMBERS : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(SMEMBERS); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class SISMEMBER : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(SISMEMBER); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class SPOP : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(SPOP); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class SRANDMEMBER : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(SRANDMEMBER); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } #endregion #region ListCommands @@ -888,33 +1077,812 @@ public override ArraySegment[] SetupSingleSlotRequest() return setup; } } - #endregion - #region LuaCommands - internal class EVAL : BaseCommand + internal class LPUSH : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LPUSH); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "a", "b", "c"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LPOP : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LPOP); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LPOS : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LPOS); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LMPOP : BaseCommand + { + public override bool IsArrayCommand => true; + public override bool ArrayResponse => true; + public override string Command => nameof(LMPOP); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return ["3", ssk[0], ssk[1], ssk[2], "LEFT"]; + } + + public override string[] GetCrossSlotRequest() + { + var csk = GetCrossSlotKeys; + return ["3", csk[0], csk[1], csk[2], "LEFT"]; + } + + public override ArraySegment[] SetupSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + var setup = new ArraySegment[3]; + setup[0] = new ArraySegment(["LPUSH", ssk[1], "value1", "value2", "value3"]); + setup[1] = new ArraySegment(["LPUSH", ssk[2], "value4", "value5", "value6"]); + setup[2] = new ArraySegment(["LPUSH", ssk[3], "value7", "value8", "value9"]); + return setup; + } + } + + internal class BLPOP : BaseCommand + { + public override bool IsArrayCommand => true; + public override bool ArrayResponse => true; + public override string Command => nameof(BLPOP); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], ssk[1], ssk[2], "1"]; + } + + public override string[] GetCrossSlotRequest() + { + var csk = GetCrossSlotKeys; + return [csk[0], csk[1], csk[2], "1"]; + } + + public override ArraySegment[] SetupSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + var setup = new ArraySegment[3]; + setup[0] = new ArraySegment(["LPUSH", ssk[1], "value1", "value2", "value3"]); + setup[1] = new ArraySegment(["LPUSH", ssk[2], "value4", "value5", "value6"]); + setup[2] = new ArraySegment(["LPUSH", ssk[3], "value7", "value8", "value9"]); + return setup; + } + } + + internal class BLMOVE : BaseCommand { public override bool IsArrayCommand => true; public override bool ArrayResponse => false; - public override string Command => nameof(EVAL); + public override string Command => nameof(BLMOVE); public override string[] GetSingleSlotRequest() { var ssk = GetSingleSlotKeys; - return ["return 'OK'", "3", ssk[0], ssk[1], ssk[2]]; + return [ssk[0], ssk[1], "LEFT", "LEFT", "1"]; } public override string[] GetCrossSlotRequest() { var csk = GetCrossSlotKeys; - return ["return 'OK'", "3", csk[0], csk[1], csk[2]]; + return [csk[0], csk[1], "LEFT", "LEFT", "1"]; } public override ArraySegment[] SetupSingleSlotRequest() { var ssk = GetSingleSlotKeys; - var setup = new ArraySegment[] { new ArraySegment(["EVAL", "return 'OK'", "3", ssk[1], ssk[2], ssk[3]]) }; + var setup = new ArraySegment[3]; + setup[0] = new ArraySegment(["LPUSH", ssk[1], "value1", "value2", "value3"]); + setup[1] = new ArraySegment(["LPUSH", ssk[2], "value4", "value5", "value6"]); + setup[2] = new ArraySegment(["LPUSH", ssk[3], "value7", "value8", "value9"]); return setup; } } + + internal class LLEN : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LLEN); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LTRIM : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LTRIM); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0", "100"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LRANGE : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(LRANGE); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0", "100"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LINDEX : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LINDEX); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LINSERT : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LINSERT); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "BEFORE", "aaa", "bbb"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class LREM : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LREM); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0", "10"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class RPOPLPUSH : BaseCommand + { + public override bool IsArrayCommand => true; + public override bool ArrayResponse => false; + public override string Command => nameof(RPOPLPUSH); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], ssk[1]]; + } + + public override string[] GetCrossSlotRequest() + { + var csk = GetCrossSlotKeys; + return [csk[0], csk[1]]; + } + + public override ArraySegment[] SetupSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + var setup = new ArraySegment[3]; + setup[0] = new ArraySegment(["LPUSH", ssk[1], "a", "b", "c"]); + setup[1] = new ArraySegment(["LPUSH", ssk[2], "d", "e", "f"]); + setup[2] = new ArraySegment(["LPUSH", ssk[3], "g", "h", "i"]); + return setup; + } + } + + internal class LSET : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(LSET); + + public override bool RequiresExistingKey => true; + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "0", "d"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + #endregion + + #region LuaCommands + internal class EVAL : BaseCommand + { + public override bool IsArrayCommand => true; + public override bool ArrayResponse => false; + public override string Command => nameof(EVAL); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return ["return 'OK'", "3", ssk[0], ssk[1], ssk[2]]; + } + + public override string[] GetCrossSlotRequest() + { + var csk = GetCrossSlotKeys; + return ["return 'OK'", "3", csk[0], csk[1], csk[2]]; + } + + public override ArraySegment[] SetupSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + var setup = new ArraySegment[] { new ArraySegment(["EVAL", "return 'OK'", "3", ssk[1], ssk[2], ssk[3]]) }; + return setup; + } + } + #endregion + + #region GeoCommands + internal class GEOADD : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(GEOADD); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "13.361389", "38.115556", "city"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class GEOHASH : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(GEOHASH); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + #endregion + + #region SortedSetCommands + internal class ZADD : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZADD); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZADD x 1 a + return [ssk[0], "1", "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZREM : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZREM); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZREM x a b c + return [ssk[0], "a", "b", "c"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZCARD : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZCARD); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZCARD x + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZRANGE : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(ZRANGE); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZRANGE x 0 -1 + return [ssk[0], "0", "-1"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZSCORE : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZSCORE); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZSCORE x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZMSCORE : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(ZMSCORE); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZMSCORE x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZPOPMAX : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(ZPOPMAX); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZPOPMAX a + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZCOUNT : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZCOUNT); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZCOUNT x 0 100 + return [ssk[0], "0", "100"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZLEXCOUNT : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZLEXCOUNT); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZLEXCOUNT x 0 100 + return [ssk[0], "0", "100"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZINCRBY : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZINCRBY); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZINCRBY x 20 a + return [ssk[0], "20", "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZRANK : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZRANK); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZRANK x a + return [ssk[0], "20"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZREMRANGEBYRANK : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZREMRANGEBYRANK); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZREMRANGEBYRANK x 0 -1 + return [ssk[0], "0", "-1"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZRANDMEMBER : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(ZRANDMEMBER); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZRANDMEMBER x + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class ZDIFF : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(ZDIFF); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // ZDIFF 2 a b + return ["2", ssk[0], ssk[1]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + #endregion + + #region HashCommands + internal class HSET : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HSET); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + //HSET x a 1 b 2 + return [ssk[0], "a", "1", "b", "2"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HGET : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HGET); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HGET x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HGETALL : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(HGETALL); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HGETALL x + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HMGET : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(HMGET); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HMGET x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HRANDFIELD : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HRANDFIELD); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HRANDFIELD x + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HLEN : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HLEN); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HLEN x + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HSTRLEN : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HSTRLEN); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HSTRLEN x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HDEL : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HDEL); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HDEL x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HEXISTS : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HEXISTS); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HEXISTS x a + return [ssk[0], "a"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HKEYS : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => true; + public override string Command => nameof(HKEYS); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HKEYS x + return [ssk[0]]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + + internal class HINCRBY : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(HINCRBY); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + // HINCRBY x a 10 + return [ssk[0], "a", "10"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } #endregion } \ No newline at end of file diff --git a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs index 63649c2360..78cd78d650 100644 --- a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs +++ b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs @@ -61,15 +61,64 @@ public class ClusterSlotVerificationTests new SINTER(), new LMOVE(), new EVAL(), + new LPUSH(), + new LPOP(), + new LMPOP(), + new BLPOP(), + new BLMOVE(), + new LLEN(), + new LTRIM(), + new LRANGE(), + new LINDEX(), + new LINSERT(), + new LREM(), + new RPOPLPUSH(), + new LSET(), + new SADD(), + new SREM(), + new SCARD(), + new SMEMBERS(), + new SISMEMBER(), + new SPOP(), + new SRANDMEMBER(), + new GEOADD(), + new GEOHASH(), + new ZADD(), + new ZREM(), + new ZCARD(), + new ZRANGE(), + new ZSCORE(), + new ZMSCORE(), + new ZPOPMAX(), + new ZCOUNT(), + new ZLEXCOUNT(), + new ZINCRBY(), + new ZRANK(), + new ZREMRANGEBYRANK(), + new ZRANDMEMBER(), + new ZDIFF(), + new HSET(), + new HGET(), + new HGETALL(), + new HMGET(), + new HRANDFIELD(), + new HLEN(), + new HSTRLEN(), + new HDEL(), + new HEXISTS(), + new HKEYS(), + new HINCRBY(), }; - ClusterTestContext context; readonly int sourceIndex = 0; readonly int targetIndex = 1; readonly int otherIndex = 2; readonly int iterations = 3; + /// + /// Issue SetSlot commands to configure slot for migration + /// private void ConfigureSlotForMigration() { var srcEndpoint = context.clusterTestUtils.GetEndPoint(sourceIndex).ToIPEndPoint(); @@ -87,6 +136,9 @@ private void ConfigureSlotForMigration() ClassicAssert.AreEqual("OK", resp); } + /// + /// Reset slot to stable state + /// private void ResetSlot() { var srcEndpoint = context.clusterTestUtils.GetEndPoint(sourceIndex).ToIPEndPoint(); @@ -164,6 +216,53 @@ public virtual void OneTimeTearDown() [TestCase("SINTERSTORE")] [TestCase("SINTER")] [TestCase("LMOVE")] + [TestCase("LPUSH")] + [TestCase("LPOP")] + [TestCase("LMPOP")] + [TestCase("BLPOP")] + [TestCase("BLMOVE")] + [TestCase("LLEN")] + [TestCase("LTRIM")] + [TestCase("LRANGE")] + [TestCase("LINDEX")] + [TestCase("LINSERT")] + [TestCase("LREM")] + [TestCase("RPOPLPUSH")] + [TestCase("LSET")] + [TestCase("SADD")] + [TestCase("SREM")] + [TestCase("SCARD")] + [TestCase("SMEMBERS")] + [TestCase("SISMEMBER")] + [TestCase("SPOP")] + [TestCase("SRANDMEMBER")] + [TestCase("GEOADD")] + [TestCase("GEOHASH")] + [TestCase("ZADD")] + [TestCase("ZREM")] + [TestCase("ZCARD")] + [TestCase("ZRANGE")] + [TestCase("ZSCORE")] + [TestCase("ZMSCORE")] + [TestCase("ZPOPMAX")] + [TestCase("ZCOUNT")] + [TestCase("ZLEXCOUNT")] + [TestCase("ZINCRBY")] + [TestCase("ZRANK")] + [TestCase("ZREMRANGEBYRANK")] + [TestCase("ZRANDMEMBER")] + [TestCase("ZDIFF")] + [TestCase("HSET")] + [TestCase("HGET")] + [TestCase("HGETALL")] + [TestCase("HMGET")] + [TestCase("HRANDFIELD")] + [TestCase("HLEN")] + [TestCase("HSTRLEN")] + [TestCase("HDEL")] + [TestCase("HEXISTS")] + [TestCase("HKEYS")] + [TestCase("HINCRBY")] public void ClusterCLUSTERDOWNTest(string commandName) { var requestNodeIndex = otherIndex; @@ -243,6 +342,53 @@ void GarnetClientSessionClusterDown(BaseCommand command) [TestCase("SINTER")] [TestCase("LMOVE")] [TestCase("EVAL")] + [TestCase("LPUSH")] + [TestCase("LPOP")] + [TestCase("LMPOP")] + [TestCase("BLPOP")] + [TestCase("BLMOVE")] + [TestCase("LLEN")] + [TestCase("LTRIM")] + [TestCase("LRANGE")] + [TestCase("LINDEX")] + [TestCase("LINSERT")] + [TestCase("LREM")] + [TestCase("RPOPLPUSH")] + [TestCase("LSET")] + [TestCase("SADD")] + [TestCase("SREM")] + [TestCase("SCARD")] + [TestCase("SMEMBERS")] + [TestCase("SISMEMBER")] + [TestCase("SPOP")] + [TestCase("SRANDMEMBER")] + [TestCase("GEOADD")] + [TestCase("GEOHASH")] + [TestCase("ZADD")] + [TestCase("ZREM")] + [TestCase("ZCARD")] + [TestCase("ZRANGE")] + [TestCase("ZSCORE")] + [TestCase("ZMSCORE")] + [TestCase("ZPOPMAX")] + [TestCase("ZCOUNT")] + [TestCase("ZLEXCOUNT")] + [TestCase("ZINCRBY")] + [TestCase("ZRANK")] + [TestCase("ZREMRANGEBYRANK")] + [TestCase("ZRANDMEMBER")] + [TestCase("ZDIFF")] + [TestCase("HSET")] + [TestCase("HGET")] + [TestCase("HGETALL")] + [TestCase("HMGET")] + [TestCase("HRANDFIELD")] + [TestCase("HLEN")] + [TestCase("HSTRLEN")] + [TestCase("HDEL")] + [TestCase("HEXISTS")] + [TestCase("HKEYS")] + [TestCase("HINCRBY")] public void ClusterOKTest(string commandName) { var requestNodeIndex = sourceIndex; @@ -333,6 +479,53 @@ void GarnetClientSessionOK(BaseCommand command) [TestCase("SINTER")] [TestCase("LMOVE")] [TestCase("EVAL")] + [TestCase("LPUSH")] + [TestCase("LPOP")] + [TestCase("LMPOP")] + [TestCase("BLPOP")] + [TestCase("BLMOVE")] + [TestCase("LLEN")] + [TestCase("LTRIM")] + [TestCase("LRANGE")] + [TestCase("LINDEX")] + [TestCase("LINSERT")] + [TestCase("LREM")] + [TestCase("RPOPLPUSH")] + [TestCase("LSET")] + [TestCase("SADD")] + [TestCase("SREM")] + [TestCase("SCARD")] + [TestCase("SMEMBERS")] + [TestCase("SISMEMBER")] + [TestCase("SPOP")] + [TestCase("SRANDMEMBER")] + [TestCase("GEOADD")] + [TestCase("GEOHASH")] + [TestCase("ZADD")] + [TestCase("ZREM")] + [TestCase("ZCARD")] + [TestCase("ZRANGE")] + [TestCase("ZSCORE")] + [TestCase("ZMSCORE")] + [TestCase("ZPOPMAX")] + [TestCase("ZCOUNT")] + [TestCase("ZLEXCOUNT")] + [TestCase("ZINCRBY")] + [TestCase("ZRANK")] + [TestCase("ZREMRANGEBYRANK")] + [TestCase("ZRANDMEMBER")] + [TestCase("ZDIFF")] + [TestCase("HSET")] + [TestCase("HGET")] + [TestCase("HGETALL")] + [TestCase("HMGET")] + [TestCase("HRANDFIELD")] + [TestCase("HLEN")] + [TestCase("HSTRLEN")] + [TestCase("HDEL")] + [TestCase("HEXISTS")] + [TestCase("HKEYS")] + [TestCase("HINCRBY")] public void ClusterCROSSSLOTTest(string commandName) { var requestNodeIndex = sourceIndex; @@ -415,6 +608,53 @@ void GarnetClientSessionCrossslotTest(BaseCommand command) [TestCase("SINTERSTORE")] [TestCase("SINTER")] [TestCase("LMOVE")] + [TestCase("LPUSH")] + [TestCase("LPOP")] + [TestCase("LMPOP")] + [TestCase("BLPOP")] + [TestCase("BLMOVE")] + [TestCase("LLEN")] + [TestCase("LTRIM")] + [TestCase("LRANGE")] + [TestCase("LINDEX")] + [TestCase("LINSERT")] + [TestCase("LREM")] + [TestCase("RPOPLPUSH")] + [TestCase("LSET")] + [TestCase("SADD")] + [TestCase("SREM")] + [TestCase("SCARD")] + [TestCase("SMEMBERS")] + [TestCase("SISMEMBER")] + [TestCase("SPOP")] + [TestCase("SRANDMEMBER")] + [TestCase("GEOADD")] + [TestCase("GEOHASH")] + [TestCase("ZADD")] + [TestCase("ZREM")] + [TestCase("ZCARD")] + [TestCase("ZRANGE")] + [TestCase("ZSCORE")] + [TestCase("ZMSCORE")] + [TestCase("ZPOPMAX")] + [TestCase("ZCOUNT")] + [TestCase("ZLEXCOUNT")] + [TestCase("ZINCRBY")] + [TestCase("ZRANK")] + [TestCase("ZREMRANGEBYRANK")] + [TestCase("ZRANDMEMBER")] + [TestCase("ZDIFF")] + [TestCase("HSET")] + [TestCase("HGET")] + [TestCase("HGETALL")] + [TestCase("HMGET")] + [TestCase("HRANDFIELD")] + [TestCase("HLEN")] + [TestCase("HSTRLEN")] + [TestCase("HDEL")] + [TestCase("HEXISTS")] + [TestCase("HKEYS")] + [TestCase("HINCRBY")] public void ClusterMOVEDTest(string commandName) { var requestNodeIndex = targetIndex; @@ -504,6 +744,53 @@ void GarnetClientSessionMOVEDTest(BaseCommand command) [TestCase("SINTERSTORE")] [TestCase("SINTER")] [TestCase("LMOVE")] + [TestCase("LPUSH")] + [TestCase("LPOP")] + [TestCase("LMPOP")] + [TestCase("BLPOP")] + [TestCase("BLMOVE")] + [TestCase("LLEN")] + [TestCase("LTRIM")] + [TestCase("LRANGE")] + [TestCase("LINDEX")] + [TestCase("LINSERT")] + [TestCase("LREM")] + [TestCase("RPOPLPUSH")] + [TestCase("LSET")] + [TestCase("SADD")] + [TestCase("SREM")] + [TestCase("SCARD")] + [TestCase("SMEMBERS")] + [TestCase("SISMEMBER")] + [TestCase("SPOP")] + [TestCase("SRANDMEMBER")] + [TestCase("GEOADD")] + [TestCase("GEOHASH")] + [TestCase("ZADD")] + [TestCase("ZREM")] + [TestCase("ZCARD")] + [TestCase("ZRANGE")] + [TestCase("ZSCORE")] + [TestCase("ZMSCORE")] + [TestCase("ZPOPMAX")] + [TestCase("ZCOUNT")] + [TestCase("ZLEXCOUNT")] + [TestCase("ZINCRBY")] + [TestCase("ZRANK")] + [TestCase("ZREMRANGEBYRANK")] + [TestCase("ZRANDMEMBER")] + [TestCase("ZDIFF")] + [TestCase("HSET")] + [TestCase("HGET")] + [TestCase("HGETALL")] + [TestCase("HMGET")] + [TestCase("HRANDFIELD")] + [TestCase("HLEN")] + [TestCase("HSTRLEN")] + [TestCase("HDEL")] + [TestCase("HEXISTS")] + [TestCase("HKEYS")] + [TestCase("HINCRBY")] public void ClusterASKTest(string commandName) { var requestNodeIndex = sourceIndex; @@ -545,7 +832,7 @@ void SERedisASKTest(BaseCommand command) catch (Exception ex) { var tokens = ex.Message.Split(' '); - ClassicAssert.IsTrue(tokens.Length > 10 && tokens[0].Equals("Endpoint"), command.Command); + ClassicAssert.IsTrue(tokens.Length > 10 && tokens[0].Equals("Endpoint"), command.Command + " => " + ex.Message); var _address = tokens[1].Split(':')[0]; var _port = int.Parse(tokens[1].Split(':')[1]); @@ -610,6 +897,53 @@ void GarnetClientSessionASKTest(BaseCommand command) [TestCase("SINTERSTORE")] [TestCase("SINTER")] [TestCase("LMOVE")] + [TestCase("LPUSH")] + [TestCase("LPOP")] + [TestCase("LMPOP")] + [TestCase("BLPOP")] + [TestCase("BLMOVE")] + [TestCase("LLEN")] + [TestCase("LTRIM")] + [TestCase("LRANGE")] + [TestCase("LINDEX")] + [TestCase("LINSERT")] + [TestCase("LREM")] + [TestCase("RPOPLPUSH")] + [TestCase("LSET")] + [TestCase("SADD")] + [TestCase("SREM")] + [TestCase("SCARD")] + [TestCase("SMEMBERS")] + [TestCase("SISMEMBER")] + [TestCase("SPOP")] + [TestCase("SRANDMEMBER")] + [TestCase("GEOADD")] + [TestCase("GEOHASH")] + [TestCase("ZADD")] + [TestCase("ZREM")] + [TestCase("ZCARD")] + [TestCase("ZRANGE")] + [TestCase("ZSCORE")] + [TestCase("ZMSCORE")] + [TestCase("ZPOPMAX")] + [TestCase("ZCOUNT")] + [TestCase("ZLEXCOUNT")] + [TestCase("ZINCRBY")] + [TestCase("ZRANK")] + [TestCase("ZREMRANGEBYRANK")] + [TestCase("ZRANDMEMBER")] + [TestCase("ZDIFF")] + [TestCase("HSET")] + [TestCase("HGET")] + [TestCase("HGETALL")] + [TestCase("HMGET")] + [TestCase("HRANDFIELD")] + [TestCase("HLEN")] + [TestCase("HSTRLEN")] + [TestCase("HDEL")] + [TestCase("HEXISTS")] + [TestCase("HKEYS")] + [TestCase("HINCRBY")] public void ClusterTRYAGAINTest(string commandName) { var requestNodeIndex = sourceIndex; diff --git a/test/Garnet.test.cluster/RedirectTests/CustomProcedure.cs b/test/Garnet.test.cluster/RedirectTests/CustomProcedure.cs new file mode 100644 index 0000000000..f1c71ab6d0 --- /dev/null +++ b/test/Garnet.test.cluster/RedirectTests/CustomProcedure.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Garnet.common; +using Garnet.server; +using Tsavorite.core; + +namespace Garnet.test.cluster +{ + /// + /// MKROTxn key1 key2 key3 key4 + /// + sealed class MultiKeyTransaction : CustomTransactionProcedure + { + public override bool Prepare(TGarnetReadApi api, ArgSlice input) + { + var offset = 0; + var setA = GetNextArg(input, ref offset); + var setB = GetNextArg(input, ref offset); + var getA = GetNextArg(input, ref offset); + var getB = GetNextArg(input, ref offset); + + AddKey(setA, LockType.Exclusive, true); + AddKey(setB, LockType.Exclusive, true); + + AddKey(getA, LockType.Shared, true); + AddKey(getB, LockType.Shared, true); + + return true; + } + + public override void Main(TGarnetApi api, ArgSlice input, ref MemoryResult output) + { + + } + } +} \ No newline at end of file From 0da61f50f0d9434cc0272a51b43ac40d90c9b7fb Mon Sep 17 00:00:00 2001 From: Badrish Chandramouli Date: Wed, 9 Oct 2024 14:59:29 -0700 Subject: [PATCH 04/15] BDN for SET variants (#710) * BDN for SET variants * update --- .../BDN.benchmark/Resp/RespParseStress.cs | 33 +++++++++++++++ libs/server/Resp/BasicCommands.cs | 41 ++++++++----------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/benchmark/BDN.benchmark/Resp/RespParseStress.cs b/benchmark/BDN.benchmark/Resp/RespParseStress.cs index a46bc8c3af..eef4ce652c 100644 --- a/benchmark/BDN.benchmark/Resp/RespParseStress.cs +++ b/benchmark/BDN.benchmark/Resp/RespParseStress.cs @@ -31,6 +31,14 @@ public unsafe class RespParseStress byte[] setexRequestBuffer; byte* setexRequestBufferPointer; + static ReadOnlySpan SETNX => "*4\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\na\r\n$2\r\nNX\r\n"u8; + byte[] setnxRequestBuffer; + byte* setnxRequestBufferPointer; + + static ReadOnlySpan SETXX => "*4\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\na\r\n$2\r\nXX\r\n"u8; + byte[] setxxRequestBuffer; + byte* setxxRequestBufferPointer; + static ReadOnlySpan GET => "*2\r\n$3\r\nGET\r\n$1\r\nb\r\n"u8; byte[] getRequestBuffer; byte* getRequestBufferPointer; @@ -91,6 +99,16 @@ public void GlobalSetup() for (int i = 0; i < batchSize; i++) SETEX.CopyTo(new Span(setexRequestBuffer).Slice(i * SETEX.Length)); + setnxRequestBuffer = GC.AllocateArray(SETNX.Length * batchSize, pinned: true); + setnxRequestBufferPointer = (byte*)Unsafe.AsPointer(ref setnxRequestBuffer[0]); + for (int i = 0; i < batchSize; i++) + SETNX.CopyTo(new Span(setnxRequestBuffer).Slice(i * SETNX.Length)); + + setxxRequestBuffer = GC.AllocateArray(SETXX.Length * batchSize, pinned: true); + setxxRequestBufferPointer = (byte*)Unsafe.AsPointer(ref setxxRequestBuffer[0]); + for (int i = 0; i < batchSize; i++) + SETXX.CopyTo(new Span(setxxRequestBuffer).Slice(i * SETXX.Length)); + getRequestBuffer = GC.AllocateArray(GET.Length * batchSize, pinned: true); getRequestBufferPointer = (byte*)Unsafe.AsPointer(ref getRequestBuffer[0]); for (int i = 0; i < batchSize; i++) @@ -121,6 +139,9 @@ public void GlobalSetup() for (int i = 0; i < batchSize; i++) HSETDEL.CopyTo(new Span(hSetDelRequestBuffer).Slice(i * HSETDEL.Length)); + // Pre-populate raw string set with a single element + SlowConsumeMessage("*3\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\na\r\n"u8); + // Pre-populate sorted set with a single element to avoid repeatedly emptying it during the benchmark SlowConsumeMessage("*4\r\n$4\r\nZADD\r\n$1\r\nc\r\n$1\r\n1\r\n$1\r\nd\r\n"u8); @@ -167,6 +188,18 @@ public void SetEx() _ = session.TryConsumeMessages(setexRequestBufferPointer, setexRequestBuffer.Length); } + [Benchmark] + public void SetNx() + { + _ = session.TryConsumeMessages(setnxRequestBufferPointer, setnxRequestBuffer.Length); + } + + [Benchmark] + public void SetXx() + { + _ = session.TryConsumeMessages(setxxRequestBufferPointer, setxxRequestBuffer.Length); + } + [Benchmark] public void Get() { diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 3e9177ad06..02f041dd8a 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -535,12 +535,6 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) return true; } - // Make space for key header - var keyPtr = sbKey.ToPointer() - sizeof(int); - - // Set key length - *(int*)keyPtr = sbKey.Length; - // Make space for value header var valPtr = sbVal.ToPointer() - sizeof(int); var vSize = sbVal.Length; @@ -553,15 +547,15 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { case ExistOptions.None: return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, true, false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, false, + : NetworkSET_EX(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, false, ref storageApi); // Can perform a blind update case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, valPtr, vSize, getValue, false, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, valPtr, vSize, getValue, false, ref storageApi); } @@ -571,15 +565,15 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { case ExistOptions.None: return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, true, true, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + : NetworkSET_EX(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, true, ref storageApi); // Can perform a blind update case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, valPtr, vSize, getValue, true, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, valPtr, vSize, getValue, true, ref storageApi); } @@ -591,13 +585,13 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { case ExistOptions.None: // We can never perform a blind update due to KEEPTTL - return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, ref sbKey, valPtr, vSize, getValue, false, ref storageApi); case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, ref sbKey, valPtr, vSize, getValue, false, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, valPtr, vSize, getValue, false, ref storageApi); } @@ -609,7 +603,7 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) return true; } - private bool NetworkSET_EX(RespCommand cmd, int expiry, byte* keyPtr, byte* valPtr, + private bool NetworkSET_EX(RespCommand cmd, int expiry, ref SpanByte key, byte* valPtr, int vsize, bool highPrecision, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { @@ -630,19 +624,20 @@ private bool NetworkSET_EX(RespCommand cmd, int expiry, byte* keyPtr : TimeSpan.FromSeconds(expiry).Ticks); } - storageApi.SET(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(valPtr)); + storageApi.SET(ref key, ref Unsafe.AsRef(valPtr)); while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); return true; } - private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byte* keyPtr, + private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref SpanByte key, byte* inputPtr, int isize, bool getValue, bool highPrecision, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { // Make space for RespCommand in input inputPtr -= RespInputHeader.Size; + var save = *(long*)inputPtr; if (expiry == 0) // no expiration provided { *(int*)inputPtr = RespInputHeader.Size + isize; @@ -670,7 +665,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt if (getValue) { var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), + var status = storageApi.SET_Conditional(ref key, ref Unsafe.AsRef(inputPtr), ref o); // Status tells us whether an old image was found during RMW or not @@ -690,7 +685,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt } else { - var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), + var status = storageApi.SET_Conditional(ref key, ref Unsafe.AsRef(inputPtr)); bool ok = status != GarnetStatus.NOTFOUND; @@ -711,7 +706,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt SendAndReset(); } } - + *(long*)inputPtr = save; return true; } From ace7cb6bc68fbe581cd3e6fa3e9ff005131c7ff8 Mon Sep 17 00:00:00 2001 From: Vijay Nirmal Date: Thu, 10 Oct 2024 08:05:52 +0530 Subject: [PATCH 05/15] [Compatibility] Added INCRBYFLOAT command (#699) * Added INCRBYFLOAT command * Fixed code format issue * Added ACL test * Fixed the warning * Removed unused import * Seprated to NetworkIncrementByFloat * Added ClusterSlotVeficationTests --------- Co-authored-by: Badrish Chandramouli --- libs/common/NumUtils.cs | 147 ++++++++++++++++++ libs/resources/RespCommandsInfo.json | 29 ++++ libs/server/Resp/BasicCommands.cs | 52 ++++++- libs/server/Resp/Parser/RespCommand.cs | 5 + libs/server/Resp/RespServerSession.cs | 1 + .../Functions/MainStore/PrivateMethods.cs | 95 +++++++++++ .../Storage/Functions/MainStore/RMWMethods.cs | 27 ++++ .../Functions/MainStore/VarLenInputMethods.cs | 47 ++++++ libs/server/Transaction/TxnKeyManager.cs | 1 + .../CommandInfoUpdater/SupportedCommand.cs | 1 + .../RedirectTests/BaseCommand.cs | 17 ++ .../ClusterSlotVerificationTests.cs | 7 + test/Garnet.test/Resp/ACL/RespCommandTests.cs | 18 +++ test/Garnet.test/RespTests.cs | 73 ++++++++- website/docs/commands/api-compatibility.md | 2 +- website/docs/commands/raw-string.md | 16 ++ 16 files changed, 532 insertions(+), 6 deletions(-) diff --git a/libs/common/NumUtils.cs b/libs/common/NumUtils.cs index 0f63146b60..98433df301 100644 --- a/libs/common/NumUtils.cs +++ b/libs/common/NumUtils.cs @@ -13,6 +13,7 @@ namespace Garnet.common public static unsafe class NumUtils { public const int MaximumFormatInt64Length = 20; // 19 + sign (i.e. -9223372036854775808) + public const int MaximumFormatDoubleLength = 310; // (i.e. -1.7976931348623157E+308) /// /// Convert long number into sequence of ASCII bytes @@ -75,6 +76,81 @@ public static unsafe void LongToBytes(long value, int length, ref byte* result) result += length; } + /// + /// Convert double number into sequence of ASCII bytes + /// + /// Value to convert + /// Span Byte + /// Length of number in result + public static int DoubleToSpanByte(double value, Span dest) + { + int totalLen = NumOfCharInDouble(value, out var integerDigits, out var signSize, out var fractionalDigits); + bool isNegative = value < 0; + if (totalLen > dest.Length) + return 0; + fixed (byte* ptr = dest) + { + byte* curr = ptr; + DoubleToBytes(value, integerDigits, fractionalDigits, ref curr); + } + + return totalLen; + } + + /// + /// Convert double number into sequence of ASCII bytes + /// + /// Value to convert + /// Number of digits in the integer part of the double value + /// Number of digits in the fractional part of the double value + /// Byte pointer, will be updated to point after the written number + public static unsafe void DoubleToBytes(double value, int integerDigits, int fractionalDigits, ref byte* result) + { + Debug.Assert(!double.IsNaN(value) && !double.IsInfinity(value), "Cannot convert NaN or Infinity to bytes."); + + if (value == 0) + { + *result++ = (byte)'0'; + return; + } + + bool isNegative = value < 0; + if (isNegative) + { + *result++ = (byte)'-'; + value = -value; + } + + result += integerDigits; + var integerPart = Math.Truncate(value); + double fractionalPart = fractionalDigits > 0 ? Math.Round(value - integerPart, fractionalDigits) : 0; + + // Convert integer part + do + { + *--result = (byte)((byte)'0' + (integerPart % 10)); + integerPart /= 10; + } while (integerPart >= 1); + result += integerDigits; + + if (fractionalDigits > 0) + { + // Add decimal point + *result++ = (byte)'.'; + + // Convert fractional part + for (int i = 0; i < fractionalDigits; i++) + { + fractionalPart *= 10; + int digit = (int)fractionalPart; + *result++ = (byte)((byte)'0' + digit); + fractionalPart = Math.Round(fractionalPart - digit, fractionalDigits - i - 1); + } + + result--; // Move back to the last digit + } + } + /// /// Convert sequence of ASCII bytes into long number /// @@ -142,6 +218,45 @@ public static bool TryBytesToLong(int length, byte* source, out long result) return true; } + /// + /// Convert sequence of ASCII bytes into double number + /// + /// Source bytes + /// Double value extracted from sequence + /// True if sequence contains only numeric digits, otherwise false + public static bool TryBytesToDouble(ReadOnlySpan source, out double result) + { + fixed (byte* ptr = source) + return TryBytesToDouble(source.Length, ptr, out result); + } + + /// + /// Convert sequence of ASCII bytes into double number + /// + /// Length of number + /// Source bytes + /// Double value extracted from sequence + /// True if sequence contains only numeric digits, otherwise false + public static bool TryBytesToDouble(int length, byte* source, out double result) + { + var fNeg = *source == '-'; + var beg = fNeg ? source + 1 : source; + var len = fNeg ? length - 1 : length; + result = 0; + + // Do not allow leading zeros + if (len > 1 && *beg == '0' && *(beg + 1) != '.') + return false; + + // Parse number and check consumed bytes to avoid alphanumeric strings + if (!TryParse(new ReadOnlySpan(beg, len), out result)) + return false; + + // Negate if parsed value has a leading negative sign + result = fNeg ? -result : result; + return true; + } + /// /// Convert sequence of ASCII bytes into ulong number /// @@ -370,6 +485,38 @@ public static int NumDigitsInLong(long v, ref bool fNeg) return 19; } + /// + /// Return number of digits in given double number incluing the decimal part and `.` character + /// + /// Double value + /// Number of digits in the integer part of the double value + public static int NumOfCharInDouble(double v, out int integerDigits, out byte signSize, out int fractionalDigits) + { + if (v == 0) + { + integerDigits = 1; + signSize = 0; + fractionalDigits = 0; + return 1; + } + + Debug.Assert(!double.IsNaN(v) && !double.IsInfinity(v)); + + signSize = (byte)(v < 0 ? 1 : 0); // Add sign if the number is negative + v = Math.Abs(v); + integerDigits = (int)Math.Log10(v) + 1; + + fractionalDigits = 0; // Max of 15 significant digits + while (fractionalDigits <= 14 && Math.Abs(v - Math.Round(v, fractionalDigits)) > 2 * Double.Epsilon) // 2 * Double.Epsilon is used to handle floating point errors + { + fractionalDigits++; + } + + var dotSize = fractionalDigits != 0 ? 1 : 0; // Add decimal point if there are significant digits + + return signSize + integerDigits + dotSize + fractionalDigits; + } + /// public static bool TryParse(ReadOnlySpan source, out int value) { diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index a46327039a..fa010ddf37 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -1848,6 +1848,35 @@ } ] }, + { + "Command": "INCRBYFLOAT", + "Name": "INCRBYFLOAT", + "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", "Name": "INFO", diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 02f041dd8a..928eb19b01 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -758,7 +758,9 @@ private bool NetworkIncrement(RespCommand cmd, ref TGarnetApi storag var output = ArgSlice.FromPinnedSpan(outputBuffer); storageApi.Increment(key, input, ref output); - var errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1 + + var errorFlag = OperationError.SUCCESS; + errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1 ? (OperationError)output.Span[0] : OperationError.SUCCESS; @@ -769,8 +771,52 @@ private bool NetworkIncrement(RespCommand cmd, ref TGarnetApi storag 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; + } + + /// + /// Increment by float (INCRBYFLOAT) + /// + private bool NetworkIncrementByFloat(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + var key = parseState.GetArgSliceByRef(0); + var sbKey = key.SpanByte; + + ArgSlice input = default; + var sbVal = parseState.GetArgSliceByRef(1).SpanByte; + var valPtr = sbVal.ToPointer() - RespInputHeader.Size; + var vSize = sbVal.Length + RespInputHeader.Size; + ((RespInputHeader*)valPtr)->cmd = RespCommand.INCRBYFLOAT; + ((RespInputHeader*)valPtr)->flags = 0; + input = new ArgSlice(valPtr, vSize); + + Span outputBuffer = stackalloc byte[NumUtils.MaximumFormatDoubleLength + 1]; + var output = ArgSlice.FromPinnedSpan(outputBuffer); + + storageApi.Increment(key, input, ref output); + + var errorFlag = OperationError.SUCCESS; + errorFlag = output.Length == NumUtils.MaximumFormatDoubleLength + 1 + ? (OperationError)output.Span[0] + : OperationError.SUCCESS; + + switch (errorFlag) + { + case OperationError.SUCCESS: + while (!RespWriteUtils.WriteBulkString(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) + SendAndReset(); + break; + case OperationError.INVALID_TYPE: + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_NOT_VALID_FLOAT, ref dcurr, + dend)) SendAndReset(); break; default: diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index cc212fa549..81f8ee8cbc 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -100,6 +100,7 @@ public enum RespCommand : byte HSETNX, INCR, INCRBY, + INCRBYFLOAT, LINSERT, LMOVE, LMPOP, @@ -1368,6 +1369,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.PEXPIRETIME; } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nINCRB"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("YFLOAT\r\n"u8)) + { + return RespCommand.INCRBYFLOAT; + } break; case 12: diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 028255b1a6..ac4df4174a 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -528,6 +528,7 @@ private bool ProcessBasicCommands(RespCommand cmd, ref TGarnetApi st RespCommand.STRLEN => NetworkSTRLEN(ref storageApi), RespCommand.INCR => NetworkIncrement(RespCommand.INCR, ref storageApi), RespCommand.INCRBY => NetworkIncrement(RespCommand.INCRBY, ref storageApi), + RespCommand.INCRBYFLOAT => NetworkIncrementByFloat(ref storageApi), RespCommand.DECR => NetworkIncrement(RespCommand.DECR, ref storageApi), RespCommand.DECRBY => NetworkIncrement(RespCommand.DECRBY, ref storageApi), RespCommand.SETBIT => NetworkStringSetBit(ref storageApi), diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 5eb0835968..c2a22d7a0c 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -396,6 +396,24 @@ static bool InPlaceUpdateNumber(long val, ref SpanByte value, ref SpanByteAndMem return true; } + static bool InPlaceUpdateNumber(double val, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo) + { + var ndigits = NumUtils.NumOfCharInDouble(val, out var _, out var _, out var _); + + if (ndigits > value.LengthWithoutMetadata) + return false; + + rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); + value.ShrinkSerializedLength(ndigits + value.MetadataSize); + _ = NumUtils.DoubleToSpanByte(val, value.AsSpan()); + rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + + Debug.Assert(output.IsSpanByte, "This code assumes it is called in-place and did not go pending"); + value.AsReadOnlySpan().CopyTo(output.SpanByte.AsSpan()); + output.SpanByte.Length = value.LengthWithoutMetadata; + return true; + } + static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, long input) { // Check if value contains a valid number @@ -415,6 +433,23 @@ static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory out return InPlaceUpdateNumber(val, ref value, ref output, ref rmwInfo, ref recordInfo); } + static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, double input) + { + // Check if value contains a valid number + if (!IsValidDouble(value.LengthWithoutMetadata, value.ToPointer(), output.SpanByte.AsSpan(), out var val)) + return true; + + val += input; + + if (!double.IsFinite(val)) + { + output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; + return true; + } + + return InPlaceUpdateNumber(val, ref value, ref output, ref rmwInfo, ref recordInfo); + } + static void CopyUpdateNumber(long next, ref SpanByte newValue, ref SpanByteAndMemory output) { NumUtils.LongToSpanByte(next, newValue.AsSpan()); @@ -422,6 +457,13 @@ static void CopyUpdateNumber(long next, ref SpanByte newValue, ref SpanByteAndMe output.SpanByte.Length = newValue.LengthWithoutMetadata; } + static void CopyUpdateNumber(double next, ref SpanByte newValue, ref SpanByteAndMemory output) + { + NumUtils.DoubleToSpanByte(next, newValue.AsSpan()); + newValue.AsReadOnlySpan().CopyTo(output.SpanByte.AsSpan()); + output.SpanByte.Length = newValue.LengthWithoutMetadata; + } + /// /// Copy update from old value to new value while also validating whether oldValue is a numerical value. /// @@ -457,6 +499,37 @@ static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, re CopyUpdateNumber(val, ref newValue, ref output); } + /// + /// Copy update from old value to new value while also validating whether oldValue is a numerical value. + /// + /// Old value copying from + /// New value copying to + /// Output value + /// Parsed input value + static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output, double input) + { + newValue.ExtraMetadata = oldValue.ExtraMetadata; + + // Check if value contains a valid number + if (!IsValidDouble(oldValue.LengthWithoutMetadata, oldValue.ToPointer(), output.SpanByte.AsSpan(), out var val)) + { + // Move to tail of the log even when oldValue is alphanumeric + // We have already paid the cost of bringing from disk so we are treating as a regular access and bring it into memory + oldValue.CopyTo(ref newValue); + return; + } + + val += input; + if (!double.IsFinite(val)) + { + output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; + return; + } + + // Move to tail of the log and update + CopyUpdateNumber(val, ref newValue, ref output); + } + /// /// Parse ASCII byte array into long and validate that only contains ASCII decimal characters /// @@ -487,6 +560,28 @@ static bool IsValidNumber(int length, byte* source, Span output, out long return true; } + static bool IsValidDouble(int length, byte* source, Span output, out double val) + { + val = 0; + try + { + // Check for valid number + if (!NumUtils.TryBytesToDouble(length, source, out val) || !double.IsFinite(val)) + { + // Signal value is not a valid number + output[0] = (byte)OperationError.INVALID_TYPE; + return false; + } + } + catch + { + // Signal value is not a valid number + output[0] = (byte)OperationError.INVALID_TYPE; + return false; + } + return true; + } + void CopyDefaultResp(ReadOnlySpan resp, ref SpanByteAndMemory dst) { if (resp.Length < dst.SpanByte.Length) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index f05a01c617..297f61118f 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -149,6 +149,14 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va return false; CopyUpdateNumber(-decrBy, ref value, ref output); break; + case RespCommand.INCRBYFLOAT: + value.UnmarkExtraMetadata(); + length = input.LengthWithoutMetadata - RespInputHeader.Size; + // Check if input contains a valid number + if (!IsValidDouble(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out double incrByFloat)) + return false; + CopyUpdateNumber(incrByFloat, ref value, ref output); + break; default: value.UnmarkExtraMetadata(); @@ -321,6 +329,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span return true; return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy); + case RespCommand.INCRBYFLOAT: + length = input.LengthWithoutMetadata - RespInputHeader.Size; + // Check if input contains a valid number + if (!IsValidDouble(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out var incrByFloat)) + return true; + return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat); + case RespCommand.SETBIT: byte* i = inputPtr + RespInputHeader.Size; byte* v = value.ToPointer(); @@ -608,6 +623,18 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy); break; + case RespCommand.INCRBYFLOAT: + length = input.LengthWithoutMetadata - RespInputHeader.Size; + // Check if input contains a valid number + if (!IsValidDouble(length, input.ToPointer() + RespInputHeader.Size, output.SpanByte.AsSpan(), out var incrByFloat)) + { + // Move to tail of the log + oldValue.CopyTo(ref newValue); + break; + } + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat); + break; + case RespCommand.SETBIT: Buffer.MemoryCopy(oldValue.ToPointer(), newValue.ToPointer(), newValue.Length, oldValue.Length); byte oldValSet = BitmapManager.UpdateBitmap(inputPtr + RespInputHeader.Size, newValue.ToPointer()); diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 6c9d812d87..f85d30de38 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -38,6 +38,33 @@ static bool IsValidNumber(int length, byte* source, out long val) return true; } + /// + /// Parse ASCII byte array into double and validate that only contains ASCII decimal characters + /// + /// Length of byte array + /// Pointer to byte array + /// Parsed long value + /// True if input contained only ASCII decimal characters, otherwise false + static bool IsValidDouble(int length, byte* source, out double val) + { + val = 0; + try + { + // Check for valid number + if (!NumUtils.TryBytesToDouble(length, source, out val) || !double.IsFinite(val)) + { + // Signal value is not a valid number + return false; + } + } + catch + { + // Signal value is not a valid number + return false; + } + return true; + } + /// public int GetRMWInitialValueLength(ref SpanByte input) { @@ -85,6 +112,14 @@ public int GetRMWInitialValueLength(ref SpanByte input) return sizeof(int) + ndigits + (fNeg ? 1 : 0); + case RespCommand.INCRBYFLOAT: + if (!IsValidDouble(input.LengthWithoutMetadata - RespInputHeader.Size, inputPtr + RespInputHeader.Size, out var incrByFloat)) + return sizeof(int); + + ndigits = NumUtils.NumOfCharInDouble(incrByFloat, out var _, out var _, out var _); + + return sizeof(int) + ndigits; + default: if (cmd >= 200) { @@ -141,6 +176,18 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) ndigits = NumUtils.NumDigitsInLong(next, ref fNeg); ndigits += fNeg ? 1 : 0; + return sizeof(int) + ndigits + t.MetadataSize; + case RespCommand.INCRBYFLOAT: + datalen = inputspan.Length - RespInputHeader.Size; + slicedInputData = inputspan.Slice(RespInputHeader.Size, datalen); + + NumUtils.TryBytesToDouble(t.AsSpan(), out var currentValue); + NumUtils.TryBytesToDouble(slicedInputData, out var incrByFloat); + var newValue = currentValue + incrByFloat; + + fNeg = false; + ndigits = NumUtils.NumOfCharInDouble(newValue, out var _, out var _, out var _); + return sizeof(int) + ndigits + t.MetadataSize; case RespCommand.SETBIT: return sizeof(int) + BitmapManager.NewBlockAllocLength(inputPtr + RespInputHeader.Size, t.Length); diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index ea0df21b08..9b30574e67 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -134,6 +134,7 @@ internal int GetKeys(RespCommand command, int inputCount, out ReadOnlySpan RespCommand.RENAME => SingleKey(1, false, LockType.Exclusive), RespCommand.INCR => SingleKey(1, false, LockType.Exclusive), RespCommand.INCRBY => SingleKey(1, false, LockType.Exclusive), + RespCommand.INCRBYFLOAT => SingleKey(1, false, LockType.Exclusive), RespCommand.DECR => SingleKey(1, false, LockType.Exclusive), RespCommand.DECRBY => SingleKey(1, false, LockType.Exclusive), RespCommand.SETBIT => SingleKey(1, false, LockType.Exclusive), diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index d8cbb9432f..5346733d2b 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -147,6 +147,7 @@ public class SupportedCommand new("HVALS", RespCommand.HVALS), new("INCR", RespCommand.INCR), new("INCRBY", RespCommand.INCRBY), + new("INCRBYFLOAT", RespCommand.INCRBYFLOAT), new("INFO", RespCommand.INFO), new("KEYS", RespCommand.KEYS), new("LASTSAVE", RespCommand.LASTSAVE), diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs index e4ef2e8fbc..0d43c96941 100644 --- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs +++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs @@ -329,6 +329,23 @@ public override string[] GetSingleSlotRequest() public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); } + internal class INCRBYFLOAT : BaseCommand + { + public override bool IsArrayCommand => false; + public override bool ArrayResponse => false; + public override string Command => nameof(INCRBYFLOAT); + + public override string[] GetSingleSlotRequest() + { + var ssk = GetSingleSlotKeys; + return [ssk[0], "1.5"]; + } + + public override string[] GetCrossSlotRequest() => throw new NotImplementedException(); + + public override ArraySegment[] SetupSingleSlotRequest() => throw new NotImplementedException(); + } + internal class APPEND : BaseCommand { public override bool IsArrayCommand => false; diff --git a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs index 78cd78d650..01ce70c8f0 100644 --- a/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs +++ b/test/Garnet.test.cluster/RedirectTests/ClusterSlotVerificationTests.cs @@ -43,6 +43,7 @@ public class ClusterSlotVerificationTests new SETRANGE(), new GETRANGE(), new INCR(), + new INCRBYFLOAT(), new APPEND(), new STRLEN(), new RENAME(), @@ -199,6 +200,7 @@ public virtual void OneTimeTearDown() [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -324,6 +326,7 @@ void GarnetClientSessionClusterDown(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -461,6 +464,7 @@ void GarnetClientSessionOK(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -591,6 +595,7 @@ void GarnetClientSessionCrossslotTest(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -727,6 +732,7 @@ void GarnetClientSessionMOVEDTest(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -880,6 +886,7 @@ void GarnetClientSessionASKTest(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 17de3946f4..4394f899f4 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -3404,6 +3404,24 @@ async Task DoIncrByAsync(GarnetClient client) } } + [Test] + public async Task IncrByFloatACLsAsync() + { + int count = 0; + + await CheckCommandsAsync( + "INCRBYFLOAT", + [DoIncrByFloatAsync] + ); + + async Task DoIncrByFloatAsync(GarnetClient client) + { + var val = await client.ExecuteForStringResultAsync("INCRBYFLOAT", [$"foo-{count}", "2"]); + count++; + ClassicAssert.AreEqual("2", val); + } + } + [Test] public async Task InfoACLsAsync() { diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index dd011797e4..adfdba4ef2 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -3,9 +3,8 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Globalization; using System.Linq; -using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -864,6 +863,76 @@ public void SimpleIncrementOverflow(RespCommand cmd) ClassicAssert.IsTrue(exception); } + [Test] + public void SimpleIncrementByFloatWithNoKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + var incrByValue = 10.5; + var expectedResult = incrByValue; + + var actualResultStr = (string)db.Execute("INCRBYFLOAT", [key, incrByValue]); + var actualResultRawStr = db.StringGet(key); + + var actualResult = double.Parse(actualResultStr, CultureInfo.InvariantCulture); + var actualResultRaw = double.Parse(actualResultRawStr, CultureInfo.InvariantCulture); + + Assert.That(actualResult, Is.EqualTo(expectedResult).Within(1.0 / Math.Pow(10, 15))); + Assert.That(actualResult, Is.EqualTo(actualResultRaw).Within(1.0 / Math.Pow(10, 15))); + } + + [Test] + [TestCase(0, 12.6)] + [TestCase(12.6, 0)] + [TestCase(10, 10)] + [TestCase(910151, 0.23659)] + [TestCase(663.12336412, 12342.3)] + [TestCase(10, -110)] + [TestCase(110, -110.234)] + [TestCase(-2110.95255555, -110.234)] + [TestCase(-2110.95255555, 100000.526654512219412)] + [TestCase(double.MaxValue, double.MinValue)] + public void SimpleIncrementByFloat(double initialValue, double incrByValue) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + db.StringSet(key, initialValue); + var expectedResult = initialValue + incrByValue; + + var actualResultStr = (string)db.Execute("INCRBYFLOAT", [key, incrByValue]); + var actualResultRawStr = db.StringGet(key); + + var actualResult = double.Parse(actualResultStr, CultureInfo.InvariantCulture); + var actualResultRaw = double.Parse(actualResultRawStr, CultureInfo.InvariantCulture); + + Assert.That(actualResult, Is.EqualTo(expectedResult).Within(1.0 / Math.Pow(10, 15))); + Assert.That(actualResult, Is.EqualTo(actualResultRaw).Within(1.0 / Math.Pow(10, 15))); + } + + [Test] + [TestCase(double.MinValue, double.MinValue)] + [TestCase(double.MaxValue, double.MaxValue)] + [TestCase("abc", 10)] + [TestCase(10, "xyz")] + public void SimpleIncrementByFloatWithInvalidFloat(object initialValue, object incrByValue) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + if (initialValue is double) + { + db.StringSet(key, (double)initialValue); + } + else if (initialValue is string) + { + db.StringSet(key, (string)initialValue); + } + + Assert.Throws(() => db.Execute("INCRBYFLOAT", key, incrByValue)); + } + [Test] public void SingleDelete() { diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index 17757c002f..cf18f38a5c 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -387,7 +387,7 @@ Note that this list is subject to change as we continue to expand our API comman | | GETSET | ➖ | | | | [INCR](raw-string.md#incr) | ➕ | | | | [INCRBY](raw-string.md#incrby) | ➕ | | -| | INCRBYFLOAT | ➖ | | +| | [INCRBYFLOAT](raw-string.md#incrbyfloat) | ➖ | | | | LCS | ➖ | | | | [MGET](raw-string.md#mget) | ➕ | | | | [MSET](raw-string.md#mset) | ➕ | | diff --git a/website/docs/commands/raw-string.md b/website/docs/commands/raw-string.md index 54e88e1dcf..651f2f0b7d 100644 --- a/website/docs/commands/raw-string.md +++ b/website/docs/commands/raw-string.md @@ -140,6 +140,22 @@ Integer reply: the value of the key after the increment. --- +### INCRBYFLOAT + +#### Syntax + +```bash + INCRBYFLOAT key increment +``` + +Increment the string representing a floating point number stored at key by the specified increment. By using a negative increment value, the result is that the value stored at the key is decremented. If the key does not exist, it is set to 0 before performing the operation. + +#### Resp Reply + +Bulk string reply: the value of the key after the increment. + +--- + ### MGET #### Syntax From e3c8a2064852d733285886252f450faa6a1e3d28 Mon Sep 17 00:00:00 2001 From: Badrish Chandramouli Date: Thu, 10 Oct 2024 15:10:40 -0700 Subject: [PATCH 06/15] Allow Upsert to configure value length based on Input, in Tsavorite. (#714) * Allow Upsert to configure value length based on Input, in Tsavorite. * fix to actually call api during creation of new record in upsert * fix test * fix ycsb value length --- .../Storage/Functions/MainStore/VarLenInputMethods.cs | 3 +++ .../Storage/Functions/ObjectStore/VarLenInputMethods.cs | 5 +++++ .../cs/benchmark/YCSB.benchmark/SessionFunctions.cs | 1 + .../Tsavorite/cs/benchmark/YCSB.benchmark/Value.cs | 2 ++ .../Tsavorite/cs/src/core/Allocator/AllocatorScan.cs | 1 + .../Tsavorite/cs/src/core/Allocator/BlittableAllocator.cs | 6 ++++++ .../cs/src/core/Allocator/BlittableAllocatorImpl.cs | 4 ++++ .../Tsavorite/cs/src/core/Allocator/GenericAllocator.cs | 6 ++++++ .../cs/src/core/Allocator/GenericAllocatorImpl.cs | 3 +++ .../storage/Tsavorite/cs/src/core/Allocator/IAllocator.cs | 4 ++++ .../Tsavorite/cs/src/core/Allocator/SpanByteAllocator.cs | 6 ++++++ .../cs/src/core/Allocator/SpanByteAllocatorImpl.cs | 8 ++++++++ .../cs/src/core/ClientSession/SessionFunctionsWrapper.cs | 3 +++ .../cs/src/core/Compaction/LogCompactionFunctions.cs | 1 + .../cs/src/core/Index/Interfaces/ISessionFunctions.cs | 5 +++++ .../cs/src/core/Index/Interfaces/SessionFunctionsBase.cs | 2 ++ .../core/Index/Tsavorite/Implementation/InternalUpsert.cs | 2 +- .../Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs | 8 ++++++++ .../Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs | 8 +++++++- libs/storage/Tsavorite/cs/test/ExpirationTests.cs | 3 +++ 20 files changed, 79 insertions(+), 2 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index f85d30de38..50199ee7ae 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -255,5 +255,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) return sizeof(int) + input.Length - RespInputHeader.Size; } + + public int GetUpsertValueLength(ref SpanByte t, ref SpanByte input) + => t.TotalSize; } } \ No newline at end of file diff --git a/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs index dff1d58284..63af04149b 100644 --- a/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs @@ -22,5 +22,10 @@ public int GetRMWInitialValueLength(ref ObjectInput input) { throw new GarnetException("GetRMWInitialValueLength is not available on the object store"); } + + public int GetUpsertValueLength(ref IGarnetObject value, ref ObjectInput input) + { + throw new GarnetException("GetUpsertInitialValueLength is not available on the object store"); + } } } \ No newline at end of file diff --git a/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/SessionFunctions.cs b/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/SessionFunctions.cs index cc4991ce9f..db536505bf 100644 --- a/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/SessionFunctions.cs +++ b/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/SessionFunctions.cs @@ -82,6 +82,7 @@ public void PostInitialUpdater(ref Key key, ref Input input, ref Value value, re public int GetRMWModifiedValueLength(ref Value value, ref Input input) => 0; public int GetRMWInitialValueLength(ref Input input) => 0; + public int GetUpsertValueLength(ref Value value, ref Input input) => Value.Size; public void PostSingleDeleter(ref Key key, ref DeleteInfo deleteInfo) { } diff --git a/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/Value.cs b/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/Value.cs index d08db3557a..0e93c207c7 100644 --- a/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/Value.cs +++ b/libs/storage/Tsavorite/cs/benchmark/YCSB.benchmark/Value.cs @@ -12,6 +12,8 @@ namespace Tsavorite.benchmark [StructLayout(LayoutKind.Explicit, Size = 8)] public struct Value { + public const int Size = 8; + [FieldOffset(0)] public long value; } diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs index 4ab5247f20..213b67956c 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs @@ -336,6 +336,7 @@ public void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput ou public int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => 0; public int GetRMWInitialValueLength(ref TInput input) => 0; + public int GetUpsertValueLength(ref TValue value, ref TInput input) => 0; public void ConvertOutputToHeap(ref TInput input, ref TOutput output) { } } diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocator.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocator.cs index f707473f41..2d45b275ed 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocator.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocator.cs @@ -104,6 +104,12 @@ public readonly (int actualSize, int allocatedSize, int keySize) GetRMWInitialRe where TSessionFunctionsWrapper : IVariableLengthInput => BlittableAllocatorImpl.GetRMWInitialRecordSize(ref key, ref input, sessionFunctions); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref TKey key, ref TValue value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + where TSessionFunctionsWrapper : IVariableLengthInput + => BlittableAllocatorImpl.GetUpsertRecordSize(ref key, ref value, ref input, sessionFunctions); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref TKey key, ref TValue value) diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocatorImpl.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocatorImpl.cs index 011143d5ce..1e6763c6c0 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocatorImpl.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/BlittableAllocatorImpl.cs @@ -102,6 +102,10 @@ public static (int actualSize, int allocatedSize, int keySize) GetRMWInitialReco [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref TKey key, ref TValue value) => (RecordSize, RecordSize, KeySize); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref TKey key, ref TValue value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + => (RecordSize, RecordSize, KeySize); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetValueLength(ref TValue value) => ValueSize; diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocator.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocator.cs index 04081fd0fe..6afdd1b8ed 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocator.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocator.cs @@ -97,6 +97,12 @@ public readonly (int actualSize, int allocatedSize, int keySize) GetRMWInitialRe where TSessionFunctionsWrapper : IVariableLengthInput => _this.GetRMWInitialRecordSize(ref key, ref input, sessionFunctions); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref TKey key, ref TValue value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + where TSessionFunctionsWrapper : IVariableLengthInput + => _this.GetUpsertRecordSize(ref key, ref value, ref input, sessionFunctions); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref TKey key, ref TValue value) => _this.GetRecordSize(ref key, ref value); diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocatorImpl.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocatorImpl.cs index 18fc8b6015..9c5a7b6b9c 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocatorImpl.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/GenericAllocatorImpl.cs @@ -151,6 +151,9 @@ internal ref TValue GetValue(long physicalAddress) internal (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref TKey key, ref TValue value) => (RecordSize, RecordSize, KeySize); + internal (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref TKey key, ref TValue value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + => (RecordSize, RecordSize, KeySize); + internal override bool TryComplete() { var b1 = objectLogDevice.TryComplete(); diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/IAllocator.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/IAllocator.cs index b22611dc8c..f7539b4b0b 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/IAllocator.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/IAllocator.cs @@ -31,6 +31,10 @@ AllocatorBase GetBase() (int actualSize, int allocatedSize, int keySize) GetRMWInitialRecordSize(ref TKey key, ref TInput input, TSessionFunctionsWrapper sessionFunctions) where TSessionFunctionsWrapper : IVariableLengthInput; + /// Get record size required for the given , , and + (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref TKey key, ref TValue value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + where TSessionFunctionsWrapper : IVariableLengthInput; + /// Get record size required for the given and (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref TKey key, ref TValue value); diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocator.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocator.cs index 9444ddd101..c2ba87b379 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocator.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocator.cs @@ -98,6 +98,12 @@ public readonly (int actualSize, int allocatedSize, int keySize) GetRMWInitialRe where TSessionFunctionsWrapper : IVariableLengthInput => _this.GetRMWInitialRecordSize(ref key, ref input, sessionFunctions); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref SpanByte key, ref SpanByte value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + where TSessionFunctionsWrapper : IVariableLengthInput + => _this.GetUpsertRecordSize(ref key, ref value, ref input, sessionFunctions); + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref SpanByte key, ref SpanByte value) => _this.GetRecordSize(ref key, ref value); diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs index baa4bb1efa..e25ac476a7 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs @@ -176,6 +176,14 @@ public int GetRequiredRecordSize(long physicalAddress, int availableBytes) return (actualSize, RoundUp(actualSize, Constants.kRecordAlignment), keySize); } + public (int actualSize, int allocatedSize, int keySize) GetUpsertRecordSize(ref SpanByte key, ref SpanByte value, ref TInput input, TSessionFunctionsWrapper sessionFunctions) + where TSessionFunctionsWrapper : IVariableLengthInput + { + int keySize = key.TotalSize; + var actualSize = RecordInfo.GetLength() + RoundUp(keySize, Constants.kRecordAlignment) + sessionFunctions.GetUpsertValueLength(ref value, ref input); + return (actualSize, RoundUp(actualSize, Constants.kRecordAlignment), keySize); + } + public (int actualSize, int allocatedSize, int keySize) GetRecordSize(ref SpanByte key, ref SpanByte value) { int keySize = key.TotalSize; diff --git a/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs b/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs index 86fbb050e8..9f3eb13825 100644 --- a/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs +++ b/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs @@ -197,6 +197,9 @@ public void UnlockTransientShared(ref TKey key, ref OperationStackContext _clientSession.functions.GetRMWModifiedValueLength(ref t, ref input); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetUpsertValueLength(ref TValue t, ref TInput input) => _clientSession.functions.GetUpsertValueLength(ref t, ref input); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public IHeapContainer GetHeapContainer(ref TInput input) { diff --git a/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs b/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs index 29cee3e636..4c7e79f8cf 100644 --- a/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs @@ -52,6 +52,7 @@ public void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput ou public int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => 0; public int GetRMWInitialValueLength(ref TInput input) => 0; + public int GetUpsertValueLength(ref TValue value, ref TInput input) => _functions.GetUpsertValueLength(ref value, ref input); /// /// No reads during compaction diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs index 2f778819cf..f9c3990984 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs @@ -189,6 +189,11 @@ public interface ISessionFunctions /// Initial expected length of value object when populated by RMW using given input /// int GetRMWInitialValueLength(ref TInput input); + + /// + /// Length of resulting value object when performing Upsert of value using given input + /// + int GetUpsertValueLength(ref TValue value, ref TInput input); #endregion Variable-length value size /// diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs index 496f234366..2d222e4b33 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs @@ -57,6 +57,8 @@ public virtual void RMWCompletionCallback(ref TKey key, ref TInput input, ref TO public virtual int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => throw new TsavoriteException("GetRMWModifiedValueLength is only available for SpanByte Functions"); /// public virtual int GetRMWInitialValueLength(ref TInput input) => throw new TsavoriteException("GetRMWInitialValueLength is only available for SpanByte Functions"); + /// + public virtual int GetUpsertValueLength(ref TValue value, ref TInput input) => throw new TsavoriteException("GetUpsertValueLength is only available for SpanByte Functions"); /// public virtual void ConvertOutputToHeap(ref TInput input, ref TOutput output) { } diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs index 9e69fd56c7..7e6c24872a 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs @@ -306,7 +306,7 @@ private OperationStatus CreateNewRecordUpsert stackCtx, ref RecordInfo srcRecordInfo) where TSessionFunctionsWrapper : ISessionFunctionsWrapper { - var (actualSize, allocatedSize, keySize) = hlog.GetRecordSize(ref key, ref value); // Input is not included in record-length calculations for Upsert + var (actualSize, allocatedSize, keySize) = hlog.GetUpsertRecordSize(ref key, ref value, ref input, sessionFunctions); AllocateOptions allocOptions = new() { Recycle = true, diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs index 9eaa027f39..121a051d53 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs @@ -19,5 +19,13 @@ public interface IVariableLengthInput /// /// int GetRMWInitialValueLength(ref TInput input); + + /// + /// Length of value object, when populated by Upsert using given value and input + /// + /// + /// + /// + int GetUpsertValueLength(ref TValue value, ref TInput input); } } \ No newline at end of file diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs index 5350548625..41d7c1b8a5 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Runtime.CompilerServices; -using static Tsavorite.core.Utility; namespace Tsavorite.core { @@ -125,5 +124,12 @@ public override int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input /// public override int GetRMWInitialValueLength(ref SpanByte input) => input.TotalSize; + + /// + /// Length of resulting object when doing Upsert with given value and input. Here we set the length to the + /// length of the provided value, ignoring input. You can provide a custom implementation for other cases. + /// + public override int GetUpsertValueLength(ref SpanByte t, ref SpanByte input) + => t.TotalSize; } } \ No newline at end of file diff --git a/libs/storage/Tsavorite/cs/test/ExpirationTests.cs b/libs/storage/Tsavorite/cs/test/ExpirationTests.cs index d47f5047fd..04adfc9bae 100644 --- a/libs/storage/Tsavorite/cs/test/ExpirationTests.cs +++ b/libs/storage/Tsavorite/cs/test/ExpirationTests.cs @@ -475,6 +475,9 @@ public override void ReadCompletionCallback(ref SpanByte key, ref ExpirationInpu /// public override int GetRMWInitialValueLength(ref ExpirationInput input) => MinValueLen; + /// + public override int GetUpsertValueLength(ref SpanByte value, ref ExpirationInput input) => value.TotalSize; + // Read functions public override unsafe bool SingleReader(ref SpanByte key, ref ExpirationInput input, ref SpanByte value, ref ExpirationOutput output, ref ReadInfo readInfo) { From 6d635c508f0e67dab825c7606baad67c1e2c4c32 Mon Sep 17 00:00:00 2001 From: Badrish Chandramouli Date: Fri, 11 Oct 2024 17:11:57 -0700 Subject: [PATCH 07/15] Correctly set IsInNewVersion bit + small null logger fix (#717) * Small fix to possibly null logger * Correctly set IsInNewVersion bit for new records when operating in (v+1) during checkpointing. Needed to ensure that we start making in-place-updates on (v+1) version instead of continuously running in append-only mode during the checkpoint. * nit * Update version to 1.0.31 --- .azure/pipelines/azure-pipelines-external-release.yml | 2 +- libs/host/GarnetServer.cs | 2 +- .../CheckpointManagement/DeviceLogCommitCheckpointManager.cs | 2 +- libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs | 4 +--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.azure/pipelines/azure-pipelines-external-release.yml b/.azure/pipelines/azure-pipelines-external-release.yml index a7de9c0fcd..94120af1b4 100644 --- a/.azure/pipelines/azure-pipelines-external-release.yml +++ b/.azure/pipelines/azure-pipelines-external-release.yml @@ -3,7 +3,7 @@ # 1) update the name: string below (line 6) -- this is the version for the nuget package (e.g. 1.0.0) # 2) update \libs\host\GarnetServer.cs readonly string version (~line 53) -- NOTE - these two values need to be the same ###################################### -name: 1.0.30 +name: 1.0.31 trigger: branches: include: diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index ce81c77b10..141cbe364c 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -52,7 +52,7 @@ public class GarnetServer : IDisposable protected StoreWrapper storeWrapper; // IMPORTANT: Keep the version in sync with .azure\pipelines\azure-pipelines-external-release.yml line ~6. - readonly string version = "1.0.30"; + readonly string version = "1.0.31"; /// /// Resp protocol version diff --git a/libs/storage/Tsavorite/cs/src/core/Index/CheckpointManagement/DeviceLogCommitCheckpointManager.cs b/libs/storage/Tsavorite/cs/src/core/Index/CheckpointManagement/DeviceLogCommitCheckpointManager.cs index c7f0528e67..5286c38bba 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/CheckpointManagement/DeviceLogCommitCheckpointManager.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/CheckpointManagement/DeviceLogCommitCheckpointManager.cs @@ -428,7 +428,7 @@ private unsafe void IOCallback(uint errorCode, uint numBytes, object context) if (errorCode != 0) { var errorMessage = new Win32Exception((int)errorCode).Message; - logger.LogError("[DeviceLogCheckpointManager] OverlappedStream GetQueuedCompletionStatus error: {errorCode} msg: {errorMessage}", errorCode, errorMessage); + logger?.LogError("[DeviceLogCheckpointManager] OverlappedStream GetQueuedCompletionStatus error: {errorCode} msg: {errorMessage}", errorCode, errorMessage); } semaphore.Release(); } diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs index b04e793e8a..e2176dcbc7 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs @@ -3,7 +3,6 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -223,8 +222,7 @@ public readonly bool IsInNewVersion } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetIsInNewVersion() => word &= ~kInNewVersionBitMask; - + public void SetIsInNewVersion() => word |= kInNewVersionBitMask; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetDirtyAndModified() => word |= kDirtyBitMask | kModifiedBitMask; [MethodImpl(MethodImplOptions.AggressiveInlining)] From cf21bd461fdbe4df0761f42bed6326813ce14feb Mon Sep 17 00:00:00 2001 From: Badrish Chandramouli Date: Sat, 12 Oct 2024 12:14:47 -0700 Subject: [PATCH 08/15] Fix Docker releases (#718) * Update Dockerfile * Update Dockerfile.alpine * Update Dockerfile.cbl-mariner * Update Dockerfile.chiseled * Update Dockerfile.ubuntu * update version for release --- .azure/pipelines/azure-pipelines-external-release.yml | 4 ++-- Dockerfile | 1 + Dockerfile.alpine | 1 + Dockerfile.cbl-mariner | 1 + Dockerfile.chiseled | 1 + Dockerfile.ubuntu | 1 + libs/host/GarnetServer.cs | 6 +++--- 7 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.azure/pipelines/azure-pipelines-external-release.yml b/.azure/pipelines/azure-pipelines-external-release.yml index 94120af1b4..a8a1dd947a 100644 --- a/.azure/pipelines/azure-pipelines-external-release.yml +++ b/.azure/pipelines/azure-pipelines-external-release.yml @@ -1,9 +1,9 @@ ###################################### # NOTE: Before running this pipeline to generate a new nuget package, update the version string in two places # 1) update the name: string below (line 6) -- this is the version for the nuget package (e.g. 1.0.0) -# 2) update \libs\host\GarnetServer.cs readonly string version (~line 53) -- NOTE - these two values need to be the same +# 2) update \libs\host\GarnetServer.cs readonly string version (~line 32) -- NOTE - these two values need to be the same ###################################### -name: 1.0.31 +name: 1.0.32 trigger: branches: include: diff --git a/Dockerfile b/Dockerfile index ef8827f1a3..15336aedbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ COPY libs/cluster/*.csproj libs/cluster/ COPY libs/common/*.csproj libs/common/ COPY libs/host/*.csproj libs/host/ COPY libs/server/*.csproj libs/server/ +COPY libs/resources/*.csproj libs/resources/ COPY libs/storage/Tsavorite/cs/src/core/*.csproj libs/storage/Tsavorite/cs/src/core/ COPY libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/*.csproj libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/ COPY main/GarnetServer/*.csproj main/GarnetServer/ diff --git a/Dockerfile.alpine b/Dockerfile.alpine index a115823fab..e2e2262ac6 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -8,6 +8,7 @@ COPY libs/cluster/*.csproj libs/cluster/ COPY libs/common/*.csproj libs/common/ COPY libs/host/*.csproj libs/host/ COPY libs/server/*.csproj libs/server/ +COPY libs/resources/*.csproj libs/resources/ COPY libs/storage/Tsavorite/cs/src/core/*.csproj libs/storage/Tsavorite/cs/src/core/ COPY libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/*.csproj libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/ COPY main/GarnetServer/*.csproj main/GarnetServer/ diff --git a/Dockerfile.cbl-mariner b/Dockerfile.cbl-mariner index 97629488f6..3b22c84d30 100644 --- a/Dockerfile.cbl-mariner +++ b/Dockerfile.cbl-mariner @@ -8,6 +8,7 @@ COPY libs/cluster/*.csproj libs/cluster/ COPY libs/common/*.csproj libs/common/ COPY libs/host/*.csproj libs/host/ COPY libs/server/*.csproj libs/server/ +COPY libs/resources/*.csproj libs/resources/ COPY libs/storage/Tsavorite/cs/src/core/*.csproj libs/storage/Tsavorite/cs/src/core/ COPY libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/*.csproj libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/ COPY main/GarnetServer/*.csproj main/GarnetServer/ diff --git a/Dockerfile.chiseled b/Dockerfile.chiseled index 9a70576c22..31a1f63124 100644 --- a/Dockerfile.chiseled +++ b/Dockerfile.chiseled @@ -12,6 +12,7 @@ COPY libs/cluster/*.csproj libs/cluster/ COPY libs/common/*.csproj libs/common/ COPY libs/host/*.csproj libs/host/ COPY libs/server/*.csproj libs/server/ +COPY libs/resources/*.csproj libs/resources/ COPY libs/storage/Tsavorite/cs/src/core/*.csproj libs/storage/Tsavorite/cs/src/core/ COPY libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/*.csproj libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/ COPY main/GarnetServer/*.csproj main/GarnetServer/ diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 69956ce040..29b4014d8a 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -8,6 +8,7 @@ COPY libs/cluster/*.csproj libs/cluster/ COPY libs/common/*.csproj libs/common/ COPY libs/host/*.csproj libs/host/ COPY libs/server/*.csproj libs/server/ +COPY libs/resources/*.csproj libs/resources/ COPY libs/storage/Tsavorite/cs/src/core/*.csproj libs/storage/Tsavorite/cs/src/core/ COPY libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/*.csproj libs/storage/Tsavorite/cs/src/devices/AzureStorageDevice/ COPY main/GarnetServer/*.csproj main/GarnetServer/ diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index 141cbe364c..32555a45ed 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -28,6 +28,9 @@ namespace Garnet /// public class GarnetServer : IDisposable { + // IMPORTANT: Keep the version in sync with .azure\pipelines\azure-pipelines-external-release.yml line ~6. + readonly string version = "1.0.32"; + internal GarnetProvider Provider; private readonly GarnetServerOptions opts; @@ -51,9 +54,6 @@ public class GarnetServer : IDisposable /// protected StoreWrapper storeWrapper; - // IMPORTANT: Keep the version in sync with .azure\pipelines\azure-pipelines-external-release.yml line ~6. - readonly string version = "1.0.31"; - /// /// Resp protocol version /// From 7e29c3a9988a03574721af07010922518b2febf6 Mon Sep 17 00:00:00 2001 From: Vijay Nirmal Date: Sun, 13 Oct 2024 23:19:27 +0530 Subject: [PATCH 09/15] [Compatibility] Added NOVALUE option for HSCAN (#701) * Added NOVALUE option for HSCAN * Review comment fix * Fixed test case faliure --------- Co-authored-by: Badrish Chandramouli --- Directory.Packages.props | 2 +- libs/server/Custom/CustomObjectBase.cs | 2 +- libs/server/Objects/Hash/HashObject.cs | 18 ++++++++++++------ libs/server/Objects/List/ListObject.cs | 2 +- libs/server/Objects/ObjectUtils.cs | 7 ++++++- libs/server/Objects/Set/SetObject.cs | 4 ++-- .../Objects/SortedSet/SortedSetObject.cs | 4 ++-- libs/server/Objects/Types/GarnetObjectBase.cs | 2 +- libs/server/Objects/Types/IGarnetObject.cs | 2 +- libs/server/Resp/CmdStrings.cs | 2 ++ main/GarnetServer/Extensions/MyDictObject.cs | 2 +- playground/GarnetJSON/JsonObject.cs | 2 +- test/Garnet.test/RespHashTests.cs | 10 ++++++++++ website/docs/commands/api-compatibility.md | 2 +- website/docs/commands/data-structures.md | 4 +++- 15 files changed, 45 insertions(+), 20 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ad93e54cd0..a346e27eca 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + diff --git a/libs/server/Custom/CustomObjectBase.cs b/libs/server/Custom/CustomObjectBase.cs index 59cf671cab..29f9a6d75b 100644 --- a/libs/server/Custom/CustomObjectBase.cs +++ b/libs/server/Custom/CustomObjectBase.cs @@ -78,7 +78,7 @@ public sealed override unsafe bool Operate(ref ObjectInput input, ref SpanByteAn // Scan Command case RespCommand.COSCAN: if (ObjectUtils.ReadScanInput(ref input, ref output, out var cursorInput, out var pattern, - out var patternLength, out var limitCount, out var error)) + out var patternLength, out var limitCount, out bool _, out var error)) { Scan(cursorInput, out var items, out var cursorOutput, count: limitCount, pattern: pattern, patternLength: patternLength); diff --git a/libs/server/Objects/Hash/HashObject.cs b/libs/server/Objects/Hash/HashObject.cs index 88d5ea9c9a..bfa3a8b410 100644 --- a/libs/server/Objects/Hash/HashObject.cs +++ b/libs/server/Objects/Hash/HashObject.cs @@ -172,10 +172,10 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory break; case HashOperation.HSCAN: if (ObjectUtils.ReadScanInput(ref input, ref output, out var cursorInput, out var pattern, - out var patternLength, out var limitCount, out var error)) + out var patternLength, out var limitCount, out bool isNoValue, out var error)) { Scan(cursorInput, out var items, out var cursorOutput, count: limitCount, pattern: pattern, - patternLength: patternLength); + patternLength: patternLength, isNoValue); ObjectUtils.WriteScanOutput(items, cursorOutput, ref output); } else @@ -203,7 +203,7 @@ private void UpdateSize(ReadOnlySpan key, ReadOnlySpan value, bool a } /// - public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0) + public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0, bool isNoValue = false) { cursor = start; items = new List(); @@ -215,7 +215,7 @@ public override unsafe void Scan(long start, out List items, out long cu } // Hashset has key and value, so count is multiplied by 2 - count *= 2; + count = isNoValue ? count : count * 2; int index = 0; foreach (var item in hash) { @@ -228,7 +228,10 @@ public override unsafe void Scan(long start, out List items, out long cu if (patternLength == 0) { items.Add(item.Key); - items.Add(item.Value); + if (!isNoValue) + { + items.Add(item.Value); + } } else { @@ -237,7 +240,10 @@ public override unsafe void Scan(long start, out List items, out long cu if (GlobUtils.Match(pattern, patternLength, keyPtr, item.Key.Length)) { items.Add(item.Key); - items.Add(item.Value); + if (!isNoValue) + { + items.Add(item.Value); + } } } } diff --git a/libs/server/Objects/List/ListObject.cs b/libs/server/Objects/List/ListObject.cs index 24d717c4ad..b807548e23 100644 --- a/libs/server/Objects/List/ListObject.cs +++ b/libs/server/Objects/List/ListObject.cs @@ -203,7 +203,7 @@ internal void UpdateSize(byte[] item, bool add = true) } /// - public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0) + public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0, bool isNoValue = false) { throw new NotImplementedException("For scan items in a list use LRANGE command"); } diff --git a/libs/server/Objects/ObjectUtils.cs b/libs/server/Objects/ObjectUtils.cs index 6f0bcfab2c..e3294fbed4 100644 --- a/libs/server/Objects/ObjectUtils.cs +++ b/libs/server/Objects/ObjectUtils.cs @@ -49,7 +49,7 @@ public static unsafe void ReallocateOutput(ref SpanByteAndMemory output, ref boo /// /// public static unsafe bool ReadScanInput(ref ObjectInput input, ref SpanByteAndMemory output, - out int cursorInput, out byte* pattern, out int patternLength, out int countInInput, out ReadOnlySpan error) + out int cursorInput, out byte* pattern, out int patternLength, out int countInInput, out bool isNoValue, out ReadOnlySpan error) { var currTokenIdx = input.parseStateStartIdx; @@ -66,6 +66,7 @@ public static unsafe bool ReadScanInput(ref ObjectInput input, ref SpanByteAndMe countInInput = 10; error = default; + isNoValue = false; while (currTokenIdx < input.parseState.Count) { @@ -90,6 +91,10 @@ public static unsafe bool ReadScanInput(ref ObjectInput input, ref SpanByteAndMe if (countInInput > limitCountInOutput) countInInput = limitCountInOutput; } + else if (sbParam.SequenceEqual(CmdStrings.NOVALUES) || sbParam.SequenceEqual(CmdStrings.novalues)) + { + isNoValue = true; + } } return true; diff --git a/libs/server/Objects/Set/SetObject.cs b/libs/server/Objects/Set/SetObject.cs index 3552588772..99f20de115 100644 --- a/libs/server/Objects/Set/SetObject.cs +++ b/libs/server/Objects/Set/SetObject.cs @@ -143,7 +143,7 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory break; case SetOperation.SSCAN: if (ObjectUtils.ReadScanInput(ref input, ref output, out var cursorInput, out var pattern, - out var patternLength, out var limitCount, out var error)) + out var patternLength, out var limitCount, out bool _, out var error)) { Scan(cursorInput, out var items, out var cursorOutput, count: limitCount, pattern: pattern, patternLength: patternLength); @@ -172,7 +172,7 @@ internal void UpdateSize(ReadOnlySpan item, bool add = true) } /// - public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0) + public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0, bool isNoValue = false) { cursor = start; items = new List(); diff --git a/libs/server/Objects/SortedSet/SortedSetObject.cs b/libs/server/Objects/SortedSet/SortedSetObject.cs index 0da710bb2f..c85cc495c3 100644 --- a/libs/server/Objects/SortedSet/SortedSetObject.cs +++ b/libs/server/Objects/SortedSet/SortedSetObject.cs @@ -304,7 +304,7 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory break; case SortedSetOperation.ZSCAN: if (ObjectUtils.ReadScanInput(ref input, ref output, out var cursorInput, out var pattern, - out var patternLength, out var limitCount, out var error)) + out var patternLength, out var limitCount, out var _, out var error)) { Scan(cursorInput, out var items, out var cursorOutput, count: limitCount, pattern: pattern, patternLength: patternLength); @@ -326,7 +326,7 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory } /// - public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0) + public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0, bool isNoValue = false) { cursor = start; items = new List(); diff --git a/libs/server/Objects/Types/GarnetObjectBase.cs b/libs/server/Objects/Types/GarnetObjectBase.cs index 95195c445a..f792b8ac4d 100644 --- a/libs/server/Objects/Types/GarnetObjectBase.cs +++ b/libs/server/Objects/Types/GarnetObjectBase.cs @@ -146,6 +146,6 @@ private bool MakeTransition(SerializationPhase expectedPhase, SerializationPhase /// A patter used to match the members of the collection /// The number of characters in the pattern /// - public abstract unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0); + public abstract unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0, bool isNoValue = false); } } \ No newline at end of file diff --git a/libs/server/Objects/Types/IGarnetObject.cs b/libs/server/Objects/Types/IGarnetObject.cs index 7ba806134c..b56cb71785 100644 --- a/libs/server/Objects/Types/IGarnetObject.cs +++ b/libs/server/Objects/Types/IGarnetObject.cs @@ -58,6 +58,6 @@ public interface IGarnetObject : IDisposable /// A patter used to match the members of the collection /// The number of characters in the pattern /// - unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0); + unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = default, int patternLength = 0, bool isNoValue = false); } } \ No newline at end of file diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index da69aba555..d4c0750b17 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -71,6 +71,8 @@ static partial class CmdStrings public static ReadOnlySpan match => "match"u8; public static ReadOnlySpan COUNT => "COUNT"u8; public static ReadOnlySpan count => "count"u8; + public static ReadOnlySpan NOVALUES => "NOVALUES"u8; + public static ReadOnlySpan novalues => "novalues"u8; public static ReadOnlySpan TYPE => "TYPE"u8; public static ReadOnlySpan type => "type"u8; public static ReadOnlySpan REGISTERCS => "REGISTERCS"u8; diff --git a/main/GarnetServer/Extensions/MyDictObject.cs b/main/GarnetServer/Extensions/MyDictObject.cs index f0eb8c605d..d87ccaadd4 100644 --- a/main/GarnetServer/Extensions/MyDictObject.cs +++ b/main/GarnetServer/Extensions/MyDictObject.cs @@ -79,7 +79,7 @@ public override void Dispose() { } /// /// /// - public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = null, int patternLength = 0) + public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = null, int patternLength = 0, bool isNoValue = false) { cursor = start; items = new(); diff --git a/playground/GarnetJSON/JsonObject.cs b/playground/GarnetJSON/JsonObject.cs index e301658cdd..840fc9d39e 100644 --- a/playground/GarnetJSON/JsonObject.cs +++ b/playground/GarnetJSON/JsonObject.cs @@ -216,6 +216,6 @@ public bool TryGet(string path, out string jsonString, ILogger? logger = null) } /// - public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = null, int patternLength = 0) => throw new NotImplementedException(); + public override unsafe void Scan(long start, out List items, out long cursor, int count = 10, byte* pattern = null, int patternLength = 0, bool isNoValue = false) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/test/Garnet.test/RespHashTests.cs b/test/Garnet.test/RespHashTests.cs index 91289dae2e..251872002d 100644 --- a/test/Garnet.test/RespHashTests.cs +++ b/test/Garnet.test/RespHashTests.cs @@ -413,6 +413,16 @@ public void CanDoHashScan() members = db.HashScan("user:user789", "*"); ClassicAssert.IsTrue(members.Count() == 5, "HSCAN with MATCH failed."); + + var fields = db.HashScanNoValues("user:user789", "*"); + ClassicAssert.IsTrue(fields.Count() == 5, "HSCAN with MATCH failed."); + CollectionAssert.AreEquivalent(new[] { "email", "email1", "email2", "email3", "age" }, fields.Select(f => f.ToString())); + + RedisResult result = db.Execute("HSCAN", "user:user789", "0", "MATCH", "*", "COUNT", "2", "NOVALUES"); + ClassicAssert.IsTrue(result.Length == 2); + var fieldsStr = ((RedisResult[]?)result[1]).Select(x => (string)x).ToArray(); + ClassicAssert.IsTrue(fieldsStr.Length == 2, "HSCAN with MATCH failed."); + CollectionAssert.AreEquivalent(new[] { "email", "email1" }, fieldsStr); } diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index cf18f38a5c..8b728581c3 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -180,7 +180,7 @@ Note that this list is subject to change as we continue to expand our API comman | | HPEXPIRETIME | ➖ | | | | HPTTL | ➖ | | | | [HRANDFIELD](data-structures.md#hrandfield) | ➕ | | -| | [HSCAN](data-structures.md#hscan) | ➕ | `NOVALUES` flag not yet implemented | +| | [HSCAN](data-structures.md#hscan) | ➕ | | | | [HSET](data-structures.md#hset) | ➕ | | | | [HSETNX](data-structures.md#hsetnx) | ➕ | | | | [HSTRLEN](data-structures.md#hstrlen) | ➕ | | diff --git a/website/docs/commands/data-structures.md b/website/docs/commands/data-structures.md index 4f0529e35b..2929978b60 100644 --- a/website/docs/commands/data-structures.md +++ b/website/docs/commands/data-structures.md @@ -159,13 +159,15 @@ The optional WITHVALUES modifier changes the reply so it includes the respective #### Syntax ```bash - HSCAN key cursor [MATCH pattern] [COUNT count] + HSCAN key cursor [MATCH pattern] [COUNT count] [NOVALUES] ``` Iterates over the fields and values of a hash stored at a given **key**. Same as [SSCAN](#sscan) and [ZSCAN](#zscan) commands, **HSCAN** is used in order to incrementally iterate over the elements of the hash set*. The **match** parameter allows to apply a filter to elements after they have been retrieved from the collection. The **count** option sets a limit to the maximum number of items returned from the server to this command. This limit is also set in conjunction with the object-scan-count-limit of the global server settings. +You can use the **NOVALUES** option to make Redis return only the keys in the hash table without their corresponding values + --- ### HSET From 274d36ef209c95ad9af2844d47ed35c65c988c05 Mon Sep 17 00:00:00 2001 From: Vijay Nirmal Date: Mon, 14 Oct 2024 05:22:21 +0530 Subject: [PATCH 10/15] Fix build warning releated to #nullable (#721) --- test/Garnet.test/RespHashTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Garnet.test/RespHashTests.cs b/test/Garnet.test/RespHashTests.cs index 251872002d..992e33fc15 100644 --- a/test/Garnet.test/RespHashTests.cs +++ b/test/Garnet.test/RespHashTests.cs @@ -420,7 +420,7 @@ public void CanDoHashScan() RedisResult result = db.Execute("HSCAN", "user:user789", "0", "MATCH", "*", "COUNT", "2", "NOVALUES"); ClassicAssert.IsTrue(result.Length == 2); - var fieldsStr = ((RedisResult[]?)result[1]).Select(x => (string)x).ToArray(); + var fieldsStr = ((RedisResult[])result[1]).Select(x => (string)x).ToArray(); ClassicAssert.IsTrue(fieldsStr.Length == 2, "HSCAN with MATCH failed."); CollectionAssert.AreEquivalent(new[] { "email", "email1" }, fieldsStr); } From 80fae44300522bf65cefaba1eb212ad12f9f79bc Mon Sep 17 00:00:00 2001 From: Tal Zaccai Date: Mon, 14 Oct 2024 18:12:34 -0600 Subject: [PATCH 11/15] Remove dynamic loading of Garnet.resources.dll + Update version to 1.0.33 (#723) * fix * format + update version * small fixes * format --- .../azure-pipelines-external-release.yml | 2 +- libs/host/GarnetServer.cs | 2 +- libs/resources/Garnet.resources.csproj | 12 ++--- libs/resources/ResourceUtils.cs | 9 ++++ libs/server/Resp/RespCommandDataCommon.cs | 46 ++++--------------- 5 files changed, 24 insertions(+), 47 deletions(-) create mode 100644 libs/resources/ResourceUtils.cs diff --git a/.azure/pipelines/azure-pipelines-external-release.yml b/.azure/pipelines/azure-pipelines-external-release.yml index a8a1dd947a..e59f629b5b 100644 --- a/.azure/pipelines/azure-pipelines-external-release.yml +++ b/.azure/pipelines/azure-pipelines-external-release.yml @@ -3,7 +3,7 @@ # 1) update the name: string below (line 6) -- this is the version for the nuget package (e.g. 1.0.0) # 2) update \libs\host\GarnetServer.cs readonly string version (~line 32) -- NOTE - these two values need to be the same ###################################### -name: 1.0.32 +name: 1.0.33 trigger: branches: include: diff --git a/libs/host/GarnetServer.cs b/libs/host/GarnetServer.cs index 32555a45ed..31850c31cd 100644 --- a/libs/host/GarnetServer.cs +++ b/libs/host/GarnetServer.cs @@ -29,7 +29,7 @@ namespace Garnet public class GarnetServer : IDisposable { // IMPORTANT: Keep the version in sync with .azure\pipelines\azure-pipelines-external-release.yml line ~6. - readonly string version = "1.0.32"; + readonly string version = "1.0.33"; internal GarnetProvider Provider; diff --git a/libs/resources/Garnet.resources.csproj b/libs/resources/Garnet.resources.csproj index f09ccdcb93..e431ae69c1 100644 --- a/libs/resources/Garnet.resources.csproj +++ b/libs/resources/Garnet.resources.csproj @@ -1,16 +1,12 @@  - net8.0 - enable - enable + true + ../../Garnet.snk + false + true - - - - - diff --git a/libs/resources/ResourceUtils.cs b/libs/resources/ResourceUtils.cs new file mode 100644 index 0000000000..5704cd579f --- /dev/null +++ b/libs/resources/ResourceUtils.cs @@ -0,0 +1,9 @@ +namespace Garnet.resources +{ + /// + /// Dummy class for externally referencing this assembly + /// + public class ResourceUtils + { + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespCommandDataCommon.cs b/libs/server/Resp/RespCommandDataCommon.cs index 5364b4283e..a0c1833bfc 100644 --- a/libs/server/Resp/RespCommandDataCommon.cs +++ b/libs/server/Resp/RespCommandDataCommon.cs @@ -1,27 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; using System.Collections.Generic; -using System.IO; -using System.Runtime.Loader; +using System.Reflection; using Garnet.common; +using Garnet.resources; using Microsoft.Extensions.Logging; namespace Garnet.server.Resp { internal class RespCommandDataCommon { - /// - /// Path to Garnet.resources.dll, where command data is found - /// - private static readonly string ResourcesAssemblyPath = Path.Combine(AppContext.BaseDirectory, @"Garnet.resources.dll"); - - /// - /// Synchronize loading and unloading of resources assembly - /// - private static readonly object ResourcesAssemblyLock = new object(); - /// /// Safely imports commands data from embedded resource in dynamically loaded/unloaded assembly /// @@ -33,32 +22,15 @@ internal class RespCommandDataCommon internal static bool TryImportRespCommandsData(string path, out IReadOnlyDictionary commandsData, ILogger logger = null) where TData : class, IRespCommandData { - lock (ResourcesAssemblyLock) - { - // Create a new unloadable assembly load context - var assemblyLoadContext = new AssemblyLoadContext(null, true); - - try - { - // Load the assembly within the context and import the data - var assembly = assemblyLoadContext.LoadFromAssemblyPath(ResourcesAssemblyPath); - - var streamProvider = StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource, null, assembly); - var commandsDocsProvider = RespCommandsDataProviderFactory.GetRespCommandsDataProvider(); + // Garnet.resources assembly, where command data is found + var resourcesAssembly = Assembly.GetAssembly(typeof(ResourceUtils)); - return commandsDocsProvider.TryImportRespCommandsData(path, - streamProvider, out commandsData, logger); - } - finally - { - // Unload the context - assemblyLoadContext.Unload(); + var streamProvider = + StreamProviderFactory.GetStreamProvider(FileLocationType.EmbeddedResource, null, resourcesAssembly); + var commandsDocsProvider = RespCommandsDataProviderFactory.GetRespCommandsDataProvider(); - // Force GC to release the loaded assembly - GC.Collect(); - GC.WaitForPendingFinalizers(); - } - } + return commandsDocsProvider.TryImportRespCommandsData(path, + streamProvider, out commandsData, logger); } } } \ No newline at end of file From 63133aa39406fc29a156d57e87d9863648626ed0 Mon Sep 17 00:00:00 2001 From: Yoganand Rajasekaran <60369795+yrajas@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:08:19 -0700 Subject: [PATCH 12/15] API to clear scratch buffer (#700) When repeated large values are retrieved from custom commands, it is possible that the scratch buffer could overflow. In such scenarios, the GetScratchBufferOffset and ResetScratchBuffer APIs could be invoked to free up the space occupied in the scratch buffer. --- libs/server/API/GarnetApi.cs | 7 + libs/server/API/GarnetWatchApi.cs | 8 + libs/server/API/IGarnetApi.cs | 13 ++ libs/server/ArgSlice/ScratchBufferManager.cs | 17 ++ libs/server/Custom/CustomFunctions.cs | 11 ++ test/Garnet.test/RespCustomCommandTests.cs | 165 +++++++++++++++++++ website/docs/extensions/procedure.md | 4 + website/docs/extensions/transactions.md | 5 + 8 files changed, 230 insertions(+) diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index ac364ad74e..a1e12775f0 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -397,6 +397,13 @@ public ITsavoriteScanIterator IterateObjectStore() public GarnetStatus ObjectScan(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter) => storageSession.ObjectScan(key, ref input, ref outputFooter, ref objectContext); + /// + public int GetScratchBufferOffset() + => storageSession.scratchBufferManager.ScratchBufferOffset; + + /// + public bool ResetScratchBuffer(int offset) + => storageSession.scratchBufferManager.ResetScratchBuffer(offset); #endregion } } \ No newline at end of file diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index 8a4e6044b2..a3ddb19a2c 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -546,6 +546,14 @@ public GarnetStatus ObjectScan(byte[] key, ref ObjectInput input, ref GarnetObje return garnetApi.ObjectScan(key, ref input, ref outputFooter); } + /// + public int GetScratchBufferOffset() + => garnetApi.GetScratchBufferOffset(); + + /// + public bool ResetScratchBuffer(int offset) + => garnetApi.ResetScratchBuffer(offset); + #endregion } } \ No newline at end of file diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 53af70fdb2..12fe0f29c5 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -1700,6 +1700,19 @@ public bool IterateObjectStore(ref TScanFunctions scanFunctions, /// GarnetStatus ObjectScan(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter); + /// + /// Retrieve the current scratch buffer offset. + /// + /// Current offset + int GetScratchBufferOffset(); + + /// + /// Resets the scratch buffer to the given offset. + /// + /// Offset to reset to + /// True if successful, else false + bool ResetScratchBuffer(int offset); + #endregion } diff --git a/libs/server/ArgSlice/ScratchBufferManager.cs b/libs/server/ArgSlice/ScratchBufferManager.cs index a3b83d728f..68861d3985 100644 --- a/libs/server/ArgSlice/ScratchBufferManager.cs +++ b/libs/server/ArgSlice/ScratchBufferManager.cs @@ -31,6 +31,9 @@ internal sealed unsafe class ScratchBufferManager /// int scratchBufferOffset; + /// Current offset in scratch buffer + internal int ScratchBufferOffset => scratchBufferOffset; + public ScratchBufferManager() { } @@ -55,6 +58,20 @@ public bool RewindScratchBuffer(ref ArgSlice slice) return false; } + /// + /// Resets scratch buffer offset to the specified offset. + /// + /// Offset to reset to + /// True if successful, else false + public bool ResetScratchBuffer(int offset) + { + if (offset < 0 || offset > scratchBufferOffset) + return false; + + scratchBufferOffset = offset; + return true; + } + /// /// Create ArgSlice in scratch buffer, from given ReadOnlySpan /// diff --git a/libs/server/Custom/CustomFunctions.cs b/libs/server/Custom/CustomFunctions.cs index dd6842e64d..0c1f41998c 100644 --- a/libs/server/Custom/CustomFunctions.cs +++ b/libs/server/Custom/CustomFunctions.cs @@ -171,6 +171,17 @@ protected static unsafe void WriteError(ref (IMemoryOwner, int) output, Re output.Item2 = len; } + /// + /// Create output as error message, from given string + /// + protected static unsafe void WriteError(ref MemoryResult output, ReadOnlySpan errorMessage) + { + var _output = (output.MemoryOwner, output.Length); + WriteError(ref _output, errorMessage); + output.MemoryOwner = _output.MemoryOwner; + output.Length = _output.Length; + } + /// /// Get argument from input, at specified offset (starting from 0) /// diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index 5949a20ce0..ad59e7e7d9 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -10,13 +10,116 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Garnet.common; using Garnet.server; using NUnit.Framework; using NUnit.Framework.Legacy; using StackExchange.Redis; +using Tsavorite.core; namespace Garnet.test { + public class LargeGet : CustomProcedure + { + public override bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + { + static bool ResetBuffer(IGarnetApi garnetApi, ref MemoryResult output, int buffOffset) + { + bool status = garnetApi.ResetScratchBuffer(buffOffset); + if (!status) + WriteError(ref output, "ERR ResetScratchBuffer failed"); + + return status; + } + + var offset = 0; + var key = GetNextArg(input, ref offset); + + var buffOffset = garnetApi.GetScratchBufferOffset(); + for (var i = 0; i < 120_000; i++) + { + garnetApi.GET(key, out var outval); + if (i % 100 == 0) + { + if (!ResetBuffer(garnetApi, ref output, buffOffset)) + return false; + } + } + + buffOffset = garnetApi.GetScratchBufferOffset(); + garnetApi.GET(key, out var outval1); + garnetApi.GET(key, out var outval2); + if (!ResetBuffer(garnetApi, ref output, buffOffset)) return false; + + buffOffset = garnetApi.GetScratchBufferOffset(); + var hashKey = GetNextArg(input, ref offset); + var field = GetNextArg(input, ref offset); + garnetApi.HashGet(hashKey, field, out var value); + if (!ResetBuffer(garnetApi, ref output, buffOffset)) return false; + + return true; + } + } + + public class LargeGetTxn : CustomTransactionProcedure + { + public override bool Prepare(TGarnetReadApi api, ArgSlice input) + { + int offset = 0; + AddKey(GetNextArg(input, ref offset), LockType.Shared, false); + return true; + } + + public override void Main(TGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + { + int offset = 0; + var key = GetNextArg(input, ref offset); + var buffOffset = garnetApi.GetScratchBufferOffset(); + for (int i = 0; i < 120_000; i++) + { + garnetApi.GET(key, out var outval); + if (i % 100 == 0) + { + if (!garnetApi.ResetScratchBuffer(buffOffset)) + { + WriteError(ref output, "ERR ResetScratchBuffer failed"); + return; + } + } + } + } + } + + public class OutOfOrderFreeBuffer : CustomProcedure + { + public override bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + { + var offset = 0; + var key = GetNextArg(input, ref offset); + + var buffOffset1 = garnetApi.GetScratchBufferOffset(); + garnetApi.GET(key, out var outval1); + + var buffOffset2 = garnetApi.GetScratchBufferOffset(); + garnetApi.GET(key, out var outval2); + + if (!garnetApi.ResetScratchBuffer(buffOffset1)) + { + WriteError(ref output, "ERR ResetScratchBuffer failed"); + return false; + } + + // Previous reset call would have shrunk the buffer. This call should fail otherwise it will expand the buffer. + if (garnetApi.ResetScratchBuffer(buffOffset2)) + { + WriteError(ref output, "ERR ResetScratchBuffer shouldn't expand the buffer"); + return false; + } + + return true; + } + } + [TestFixture] public class RespCustomCommandTests { @@ -547,6 +650,68 @@ public void CustomCommandRegistrationTest() ClassicAssert.AreEqual("30", retValue.ToString()); } + [Test] + public void CustomProcedureFreeBufferTest() + { + server.Register.NewProcedure("LARGEGET", new LargeGet()); + var key = "key"; + var hashKey = "hashKey"; + var hashField = "field"; + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + byte[] value = new byte[10_000]; + db.StringSet(key, value); + db.HashSet(hashKey, [new HashEntry(hashField, value)]); + + try + { + var result = db.Execute("LARGEGET", key, hashKey, hashField); + ClassicAssert.AreEqual("OK", result.ToString()); + } + catch (RedisServerException rse) + { + ClassicAssert.Fail(rse.Message); + } + } + + [Test] + public void CustomTxnFreeBufferTest() + { + server.Register.NewTransactionProc("LARGEGETTXN", () => new LargeGetTxn()); + var key = "key"; + var hashKey = "hashKey"; + var hashField = "field"; + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + byte[] value = new byte[10_000]; + db.StringSet(key, value); + db.HashSet(hashKey, [new HashEntry(hashField, value)]); + + try + { + var result = db.Execute("LARGEGETTXN", key); + ClassicAssert.AreEqual("OK", result.ToString()); + } + catch (RedisServerException rse) + { + ClassicAssert.Fail(rse.Message); + } + } + + [Test] + public void CustomProcedureOutOfOrderFreeBufferTest() + { + server.Register.NewProcedure("OUTOFORDERFREE", new OutOfOrderFreeBuffer()); + var key = "key"; + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + byte[] value = new byte[10_000]; + db.StringSet(key, value); + + var result = db.Execute("OUTOFORDERFREE", key); + ClassicAssert.AreEqual("OK", result.ToString()); + } + private string[] CreateTestLibraries() { var runtimePath = RuntimeEnvironment.GetRuntimeDirectory(); diff --git a/website/docs/extensions/procedure.md b/website/docs/extensions/procedure.md index 70a13afd33..ba3178707b 100644 --- a/website/docs/extensions/procedure.md +++ b/website/docs/extensions/procedure.md @@ -24,6 +24,10 @@ Registering the custom procedure is done on the server-side by calling the method on the Garnet server object's `RegisterAPI` object with its name, an instance of the custom procedure class and optional commandInfo. +**NOTE** When invoking APIs on `IGarnetApi` multiple times with large outputs, it is possible to exhaust the internal buffer capacity. If such usage scenarios are expected, the buffer could be reset as described below. +* Retrieve the initial buffer offset using `IGarnetApi.GetScratchBufferOffset` +* Invoke necessary apis on `IGarnetApi` +* Reset the buffer back to where it was using `IGarnetApi.ResetScratchBuffer(offset)` :::tip As a reference of an implementation of a custom procedure, see the example in GarnetServer\Extensions\Sum.cs. diff --git a/website/docs/extensions/transactions.md b/website/docs/extensions/transactions.md index 5c91355926..c97576c91c 100644 --- a/website/docs/extensions/transactions.md +++ b/website/docs/extensions/transactions.md @@ -26,6 +26,11 @@ These are the helper methods for developing custom transactions. - `GetNextArg(ArgSlice input, ref int offset)` This method is used to retrieve the next argument from the input at the specified offset. It takes an ArgSlice parameter representing the input and a reference to an int offset. It returns an ArgSlice object representing the argument as a span. The method internally reads a pointer with a length header to extract the argument. These member functions provide utility and convenience methods for manipulating and working with the transaction data, scratch buffer, and input arguments within the CustomTransactionProcedure class. +**NOTE** When invoking APIs on `IGarnetApi` multiple times with large outputs, it is possible to exhaust the internal buffer capacity. If such usage scenarios are expected, the buffer could be reset as described below. +* Retrieve the initial buffer offset using `IGarnetApi.GetScratchBufferOffset` +* Invoke necessary apis on `IGarnetApi` +* Reset the buffer back to where it was using `IGarnetApi.ResetScratchBuffer(offset)` + Registering the custom transaction is done on the server-side by calling the `NewTransactionProc(string name, int numParams, Func proc)` method on the Garnet server object's `RegisterAPI` object with its name, number of parameters and a method that returns an instance of the custom transaction class.\ It is possible to register the custom transaction from the client-side as well (as an admin command, given that the code already resides on the server) by using the `REGISTER` command (see [Custom Commands](../dev/custom-commands.md)). From 14083b05496db08f1c380836ab589c873994f9f6 Mon Sep 17 00:00:00 2001 From: Yoganand Rajasekaran <60369795+yrajas@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:52:58 -0700 Subject: [PATCH 13/15] Use generics for CustomProcedure. (#725) To ensure high performance when executing custom procedures, the Execute method is modified to utilize generic type of GarnetApi instead of as a parameter that needs an interface call and boxing. --- libs/server/Custom/CustomProcedureWrapper.cs | 3 ++- main/GarnetServer/Extensions/SetStringAndList.cs | 2 +- main/GarnetServer/Extensions/Sum.cs | 2 +- test/Garnet.test/RespCustomCommandTests.cs | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/libs/server/Custom/CustomProcedureWrapper.cs b/libs/server/Custom/CustomProcedureWrapper.cs index 70a7dc5ddf..b0e5852a01 100644 --- a/libs/server/Custom/CustomProcedureWrapper.cs +++ b/libs/server/Custom/CustomProcedureWrapper.cs @@ -15,7 +15,8 @@ public abstract class CustomProcedure : CustomFunctions /// Custom command implementation /// /// - public abstract bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output); + public abstract bool Execute(TGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + where TGarnetApi : IGarnetApi; } class CustomProcedureWrapper diff --git a/main/GarnetServer/Extensions/SetStringAndList.cs b/main/GarnetServer/Extensions/SetStringAndList.cs index c8ddfba24b..0b7ccd49a4 100644 --- a/main/GarnetServer/Extensions/SetStringAndList.cs +++ b/main/GarnetServer/Extensions/SetStringAndList.cs @@ -8,7 +8,7 @@ namespace Garnet { class SetStringAndList : CustomProcedure { - public override bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + public override bool Execute(TGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) { var offset = 0; var key = GetNextArg(input, ref offset); diff --git a/main/GarnetServer/Extensions/Sum.cs b/main/GarnetServer/Extensions/Sum.cs index 3e227cfe10..37ce9607e1 100644 --- a/main/GarnetServer/Extensions/Sum.cs +++ b/main/GarnetServer/Extensions/Sum.cs @@ -8,7 +8,7 @@ namespace Garnet { class Sum : CustomProcedure { - public override bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + public override bool Execute(TGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) { var offset = 0; var sum = 0; diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index ad59e7e7d9..8b211d5559 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -21,9 +21,9 @@ namespace Garnet.test { public class LargeGet : CustomProcedure { - public override bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + public override bool Execute(TGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) { - static bool ResetBuffer(IGarnetApi garnetApi, ref MemoryResult output, int buffOffset) + static bool ResetBuffer(TGarnetApi garnetApi, ref MemoryResult output, int buffOffset) { bool status = garnetApi.ResetScratchBuffer(buffOffset); if (!status) @@ -92,7 +92,7 @@ public override void Main(TGarnetApi garnetApi, ArgSlice input, ref public class OutOfOrderFreeBuffer : CustomProcedure { - public override bool Execute(IGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) + public override bool Execute(TGarnetApi garnetApi, ArgSlice input, ref MemoryResult output) { var offset = 0; var key = GetNextArg(input, ref offset); From 098c7cdb3fe0bd1c649f70159dac20232f917a99 Mon Sep 17 00:00:00 2001 From: Vasileios Zois <96085550+vazois@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:38:00 -0700 Subject: [PATCH 14/15] fix bitpos bug (#724) --- libs/server/Resp/Bitmap/BitmapManagerBitPos.cs | 2 +- test/Garnet.test/GarnetBitmapTests.cs | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index 7e002e48e4..58820052cd 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -183,7 +183,7 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long payload = (bSetVal == 0) ? ~payload : payload; if (payload == mask) - return -1; + return pos + 0; pos += (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); diff --git a/test/Garnet.test/GarnetBitmapTests.cs b/test/Garnet.test/GarnetBitmapTests.cs index 88ad67c31c..bb05a2eecf 100644 --- a/test/Garnet.test/GarnetBitmapTests.cs +++ b/test/Garnet.test/GarnetBitmapTests.cs @@ -2263,10 +2263,10 @@ public void BitmapBitPosFixedTests() ClassicAssert.AreEqual(16, pos); pos = db.StringBitPosition(key, true, 0, 0, StringIndexType.Byte); - ClassicAssert.AreEqual(-1, pos); + ClassicAssert.AreEqual(0, pos); pos = db.StringBitPosition(key, false, 0, 0, StringIndexType.Byte); - ClassicAssert.AreEqual(-1, pos); + ClassicAssert.AreEqual(0, pos); value = [0xf8, 0x6f, 0xf0]; db.StringSet(key, value); @@ -2275,9 +2275,17 @@ public void BitmapBitPosFixedTests() pos = db.StringBitPosition(key, true, 10, 12, StringIndexType.Bit); ClassicAssert.AreEqual(10, pos); + + key = "mykey2"; + db.StringSetBit(key, 63, false); + pos = db.StringBitPosition(key, false, 1); + ClassicAssert.AreEqual(8, pos); + + pos = db.StringBitPosition(key, false, 0); + ClassicAssert.AreEqual(0, pos); } - [Test, Order(34)] + [Test, Order(35)] [Category("BITOP")] public void BitmapOperationNonExistentSourceKeys() { @@ -2290,7 +2298,7 @@ public void BitmapOperationNonExistentSourceKeys() ClassicAssert.AreEqual(0, size); } - [Test, Order(35)] + [Test, Order(36)] [Category("BITOP")] public void BitmapOperationInvalidOption() { @@ -2309,7 +2317,7 @@ public void BitmapOperationInvalidOption() } } - [Test, Order(36)] + [Test, Order(37)] [Category("BITOP")] public void BitmapOperationTooManyKeys() { From 71fb5b700979f47b0d7300b19b6f1ac9546b12fd Mon Sep 17 00:00:00 2001 From: Badrish Chandramouli Date: Wed, 16 Oct 2024 11:52:09 -0700 Subject: [PATCH 15/15] Fix Persist RMW size logic, add fine grained address testing (#730) * Fix Persist RMW size logic, add fine grained address testing * nits * clean up test * nit --- .../Functions/MainStore/VarLenInputMethods.cs | 3 +- test/Garnet.test/RespLowMemoryTests.cs | 118 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 test/Garnet.test/RespLowMemoryTests.cs diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 50199ee7ae..0e489aa78e 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -213,8 +213,9 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) case RespCommand.SET: case RespCommand.SETEXXX: - case RespCommand.PERSIST: break; + case RespCommand.PERSIST: + return sizeof(int) + t.LengthWithoutMetadata; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: diff --git a/test/Garnet.test/RespLowMemoryTests.cs b/test/Garnet.test/RespLowMemoryTests.cs new file mode 100644 index 0000000000..0b1d696bbf --- /dev/null +++ b/test/Garnet.test/RespLowMemoryTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using StackExchange.Redis; + +namespace Garnet.test +{ + struct StoreAddressInfo + { + public long BeginAddress; + public long HeadAddress; + public long ReadOnlyAddress; + public long TailAddress; + } + + [TestFixture] + public class RespLowMemoryTests + { + GarnetServer server; + + [SetUp] + public void Setup() + { + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true); + server.Start(); + } + + [TearDown] + public void TearDown() + { + server.Dispose(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir); + } + + StoreAddressInfo GetStoreAddressInfo(IServer server) + { + StoreAddressInfo result = default; + var info = server.Info("STORE"); + foreach (var section in info) + { + foreach (var entry in section) + { + if (entry.Key.Equals("Log.BeginAddress")) + result.BeginAddress = long.Parse(entry.Value); + else if (entry.Key.Equals("Log.HeadAddress")) + result.HeadAddress = long.Parse(entry.Value); + else if (entry.Key.Equals("Log.SafeReadOnlyAddress")) + result.ReadOnlyAddress = long.Parse(entry.Value); + else if (entry.Key.Equals("Log.TailAddress")) + result.TailAddress = long.Parse(entry.Value); + } + } + return result; + } + + void MakeReadOnly(long untilAddress, IServer server, IDatabase db) + { + var i = 1000; + var info = GetStoreAddressInfo(server); + + // Add keys so that the first record enters the read-only region + // Each record is 40 bytes here, because they do not have expirations + while (info.ReadOnlyAddress < untilAddress) + { + var key = $"key{i++:00000}"; + _ = db.StringSet(key, key); + info = GetStoreAddressInfo(server); + } + } + + [Test] + public void PersistCopyUpdateTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig(allowAdmin: true)); + var db = redis.GetDatabase(0); + var server = redis.GetServer(TestUtils.Address, TestUtils.Port); + var info = GetStoreAddressInfo(server); + + // Start at tail address of 64 + ClassicAssert.AreEqual(64, info.TailAddress); + + var expire = 100; + var key0 = $"key{0:00000}"; + _ = db.StringSet(key0, key0, TimeSpan.FromSeconds(expire)); + + // Record size for key0 is 8 bytes header + 16 bytes key + 16 bytes value + 8 bytes expiry = 48 bytes + // so the new tail address should be 64 + 48 = 112 + // That is, key0 is located at [64, 112) + info = GetStoreAddressInfo(server); + ClassicAssert.AreEqual(112, info.TailAddress); + + // Make the record read-only by adding more records + MakeReadOnly(info.TailAddress, server, db); + + info = GetStoreAddressInfo(server); + var previousTail = info.TailAddress; + + // The first record inserted (key0) is now read-only + ClassicAssert.IsTrue(info.ReadOnlyAddress >= 112); + + // Persist the key, which should cause RMW to CopyUpdate to tail + var response = db.KeyPersist(key0); + ClassicAssert.IsTrue(response); + + // Now key0 is only 40 bytes, as we are removing the expiration + // That is, key0 is now moved to [previousTail, previousTail + 40) + info = GetStoreAddressInfo(server); + ClassicAssert.AreEqual(previousTail + 40, info.TailAddress); + + // Verify that key0 exists with correct value + ClassicAssert.AreEqual(key0, (string)db.StringGet(key0)); + } + } +} \ No newline at end of file