From 1c23babe83e6f94a7143c64d313dbfcf928dd7fa Mon Sep 17 00:00:00 2001 From: ProTip Date: Sun, 24 Mar 2024 15:24:34 -0500 Subject: [PATCH 1/7] Add support for ZMSCORE command --- libs/server/API/GarnetApiObjectCommands.cs | 4 + libs/server/API/GarnetWatchApi.cs | 7 ++ libs/server/API/IGarnetApi.cs | 42 +++++---- .../Objects/SortedSet/SortedSetObject.cs | 6 +- .../Objects/SortedSet/SortedSetObjectImpl.cs | 55 +++++++++++- libs/server/Resp/Objects/SortedSetCommands.cs | 88 +++++++++++++++++-- libs/server/Resp/RespCommand.cs | 8 +- libs/server/Resp/RespCommandsInfo.cs | 1 + libs/server/Resp/RespInfo.cs | 2 +- libs/server/Resp/RespServerSession.cs | 3 +- .../Session/ObjectStore/SortedSetOps.cs | 22 ++++- libs/server/Transaction/TxnKeyManager.cs | 1 + test/Garnet.test/RespSortedSetTests.cs | 43 ++++++--- 13 files changed, 239 insertions(+), 43 deletions(-) diff --git a/libs/server/API/GarnetApiObjectCommands.cs b/libs/server/API/GarnetApiObjectCommands.cs index b76ad884a6..97176686fb 100644 --- a/libs/server/API/GarnetApiObjectCommands.cs +++ b/libs/server/API/GarnetApiObjectCommands.cs @@ -64,6 +64,10 @@ public GarnetStatus SortedSetRange(byte[] key, ArgSlice input, ref GarnetObjectS public GarnetStatus SortedSetScore(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter) => storageSession.SortedSetScore(key, input, ref outputFooter, ref objectContext); + /// + public GarnetStatus SortedSetScores(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter) + => storageSession.SortedSetScores(key, input, ref outputFooter, ref objectContext); + /// public GarnetStatus SortedSetPop(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter) => storageSession.SortedSetPop(key, input, ref outputFooter, ref objectContext); diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index f164b2994f..9fc33e0e16 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -128,6 +128,13 @@ public GarnetStatus SortedSetScore(byte[] key, ArgSlice input, ref GarnetObjectS return garnetApi.SortedSetScore(key, input, ref outputFooter); } + /// + public GarnetStatus SortedSetScores(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter) + { + garnetApi.WATCH(key, StoreType.Object); + return garnetApi.SortedSetScores(key, input, ref outputFooter); + } + /// public GarnetStatus SortedSetRank(byte[] key, ArgSlice input, out ObjectOutputHeader output) { diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 6792f93017..db364db328 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -251,7 +251,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// Name of the key or object to get the memory usage /// The value in bytes the key or object is using - /// Number of sampled nested values + /// Number of sampled nested values /// GarnetStatus GarnetStatus MemoryUsageForKey(ArgSlice key, out long memoryUsage, int samples = 0); @@ -335,7 +335,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi GarnetStatus SortedSetRemoveRangeByLex(byte[] key, ArgSlice input, out ObjectOutputHeader output); /// - /// Removes and returns the first element from the sorted set stored at key, + /// Removes and returns the first element from the sorted set stored at key, /// with the scores ordered from low to high (min) or high to low (max). /// /// @@ -355,7 +355,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi GarnetStatus SortedSetPop(ArgSlice key, out (ArgSlice score, ArgSlice member)[] pairs, int count = 1, bool lowScoresFirst = true); /// - /// Increments the score of member in the sorted set stored at key by increment. + /// Increments the score of member in the sorted set stored at key by increment. /// If member does not exist in the sorted set, it is added with increment as its score (as if its previous score was 0.0). /// /// @@ -442,7 +442,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// Adds the specified members to the set at key. - /// Specified members that are already a member of this set are ignored. + /// Specified members that are already a member of this set are ignored. /// If key does not exist, a new set is created. /// /// @@ -453,7 +453,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// Adds the specified members to the set at key. - /// Specified members that are already a member of this set are ignored. + /// Specified members that are already a member of this set are ignored. /// If key does not exist, a new set is created. /// /// @@ -464,7 +464,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// Removes the specified member from the set. - /// Specified members that are not a member of this set are ignored. + /// Specified members that are not a member of this set are ignored. /// If key does not exist, this command returns 0. /// /// @@ -475,7 +475,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// Removes the specified members from the set. - /// Specified members that are not a member of this set are ignored. + /// Specified members that are not a member of this set are ignored. /// If key does not exist, this command returns 0. /// /// @@ -486,7 +486,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// Removes the specified members from the set. - /// Specified members that are not a member of this set are ignored. + /// Specified members that are not a member of this set are ignored. /// If key does not exist, this command returns 0. /// /// @@ -726,7 +726,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi GarnetStatus HashSet(byte[] key, ArgSlice input, out ObjectOutputHeader output); /// - /// Set only if field does not yet exist. If key does not exist, a new key holding a hash is created. + /// Set only if field does not yet exist. If key does not exist, a new key holding a hash is created. /// If field already exists, no action is performed. /// HashSet only when field does not exist /// @@ -788,7 +788,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi #region BitMaps Methods /// - /// + /// /// /// /// @@ -818,7 +818,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi GarnetStatus StringBitOperation(ArgSlice[] keys, BitmapOperation bitop, out long result); /// - /// Perform a bitwise operation between multiple keys + /// Perform a bitwise operation between multiple keys /// and store the result in the destination key. /// /// @@ -865,7 +865,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi GarnetStatus HyperLogLogAdd(ArgSlice keys, string[] elements, out bool updated); /// - /// Merge multiple HyperLogLog values into a unique value that will approximate the cardinality + /// Merge multiple HyperLogLog values into a unique value that will approximate the cardinality /// of the union of the observed Sets of the source HyperLogLog structures. /// /// @@ -986,6 +986,16 @@ public interface IGarnetReadApi /// GarnetStatus SortedSetScore(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter); + /// + /// Returns the score of member in the sorted set at key. + /// If member does not exist in the sorted set, or key does not exist, nil is returned. + /// + /// + /// + /// + /// + GarnetStatus SortedSetScores(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter); + /// /// Returns the number of elements in the sorted set at key with a score between min and max. /// @@ -997,7 +1007,7 @@ public interface IGarnetReadApi /// /// Returns the number of elements in the sorted set with a value between min and max. - /// When all the elements in a sorted set have the same score, + /// When all the elements in a sorted set have the same score, /// this command forces lexicographical ordering. /// /// @@ -1324,7 +1334,7 @@ public interface IGarnetReadApi GarnetStatus StringGetBit(ArgSlice key, ArgSlice offset, out bool bValue); /// - /// Count the number of set bits in a string. + /// Count the number of set bits in a string. /// It can be specified an interval for counting, passing the start and end arguments. /// /// @@ -1334,7 +1344,7 @@ public interface IGarnetReadApi GarnetStatus StringBitCount(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output); /// - /// Count the number of set bits in a string. + /// Count the number of set bits in a string. /// It can be specified an interval for counting, passing the start and end arguments. /// /// @@ -1379,7 +1389,7 @@ public interface IGarnetReadApi GarnetStatus HyperLogLogLength(ArgSlice[] keys, ref SpanByte input, out long count, out bool error); /// - /// + /// /// /// /// diff --git a/libs/server/Objects/SortedSet/SortedSetObject.cs b/libs/server/Objects/SortedSet/SortedSetObject.cs index 63e0afa289..49ba3de386 100644 --- a/libs/server/Objects/SortedSet/SortedSetObject.cs +++ b/libs/server/Objects/SortedSet/SortedSetObject.cs @@ -43,6 +43,7 @@ public enum SortedSetOperation : byte ZRANDMEMBER, ZDIFF, ZSCAN, + ZMSCORE } /// @@ -204,6 +205,9 @@ public override unsafe bool Operate(ref SpanByte input, ref SpanByteAndMemory ou case SortedSetOperation.ZSCORE: SortedSetScore(_input, ref output); break; + case SortedSetOperation.ZMSCORE: + SortedSetScores(_input, input.Length, ref output); + break; case SortedSetOperation.ZCOUNT: SortedSetCount(_input, input.Length, _output); break; @@ -381,7 +385,7 @@ public static void InPlaceDiff(Dictionary dict1, Dictionarycount; + + bool isMemory = false; + MemoryHandle ptrHandle = default; + + byte* outPtr = output.SpanByte.ToPointer(); + var outCurr = outPtr; + var outEnd = outCurr + output.Length; + + byte* startptr = input + sizeof(ObjectInputHeader); + byte* inputCurr = startptr; + byte* inputEnd = input + length; + try + { + while (!RespWriteUtils.WriteArrayLength(count, ref outCurr, outEnd)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + + for (int c = 0; c < count; c++) + { + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var scoreKey, ref inputCurr, inputEnd)) + return; + if (!sortedSetDict.TryGetValue(scoreKey, out var score)) + { + while (!RespWriteUtils.WriteNull(ref outCurr, outEnd)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + } + else + { + while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(score.ToString()), ref outCurr, outEnd)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + } + } + _output.bytesDone = (int)(inputCurr - startptr); + _output.countDone = count; + } + finally + { + while (!RespWriteUtils.WriteDirect(ref _output, ref outCurr, outEnd)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + + if (isMemory) ptrHandle.Dispose(); + output.Length = (int)(outCurr - outPtr); + } + } + private void SortedSetCount(byte* input, int length, byte* output) { var _input = (ObjectInputHeader*)input; @@ -287,7 +338,7 @@ private void SortedSetRank(byte* input, int length, byte* output) private void SortedSetRange(byte* input, int length, ref SpanByteAndMemory output) { //ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES] - //ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] + //ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] var _input = (ObjectInputHeader*)input; int count = _input->count; @@ -747,7 +798,7 @@ private void GetRangeOrCountByLex(byte* input, int length, byte* output, SortedS } /// - /// Gets the rank of a member of the sorted set + /// Gets the rank of a member of the sorted set /// in ascending or descending order /// /// diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index 5e91186498..d7eb6799ec 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -354,7 +354,7 @@ private unsafe bool SortedSetRange(int count, byte* ptr, SortedSetOp } /// - /// Returns the specified range of elements in the sorted set stored at key. + /// Returns the specified range of elements in the sorted set stored at key. /// The ordering is reversed. /// /// @@ -450,7 +450,83 @@ private unsafe bool SortedSetScore(int count, byte* ptr, ref TGarnet } /// - /// Removes and returns the first element from the sorted set stored at key, + /// Returns the score of member in the sorted set at key. + /// If member does not exist in the sorted set, or key does not exist, nil is returned. + /// + /// + /// + /// + /// + /// + private unsafe bool SortedSetScores(int count, byte* ptr, ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + ptr += 13; + + //validation if minimum args + if (count - 2 == 0) + { + // send error to output + return AbortWithWrongNumberOfArguments("ZMSCORE", count); + } + else + { + // Get the key for SortedSet + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, recvBufferPtr + bytesRead)) + return false; + + if (NetworkSingleKeySlotVerify(key, true)) + { + var bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); + if (!DrainCommands(bufSpan, count)) return true; + return true; + } + + // Prepare input + var inputPtr = (ObjectInputHeader*)(ptr - sizeof(ObjectInputHeader)); + + //save values + var save = *inputPtr; + + // Prepare length of header in input buffer + var inputLength = (int)(recvBufferPtr + bytesRead - (byte*)inputPtr); + + int inputCount = count - 2; + // Prepare header in input buffer + inputPtr->header.type = GarnetObjectType.SortedSet; + inputPtr->header.SortedSetOp = SortedSetOperation.ZMSCORE; + inputPtr->count = inputCount; + inputPtr->done = 0; + + // Prepare GarnetObjectStore output + var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) }; + + var status = storageApi.SortedSetScore(key, new ArgSlice((byte*)inputPtr, inputLength), ref outputFooter); + + //restore input + *inputPtr = save; + + switch (status) + { + case GarnetStatus.OK: + //process output + var objOutputHeader = ProcessOutputWithHeader(outputFooter.spanByteAndMemory); + ptr += objOutputHeader.bytesDone; + break; + case GarnetStatus.NOTFOUND: + while (!RespWriteUtils.WriteResponse(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + } + } + + // Move input head + readHead = (int)(ptr - recvBufferPtr); + return true; + } + + /// + /// Removes and returns the first element from the sorted set stored at key, /// with the scores ordered from low to high (min) or high to low (max). /// /// @@ -625,9 +701,9 @@ private unsafe bool SortedSetCount(int count, byte* ptr, ref TGarnet /// /// ZLEXCOUNT: Returns the number of elements in the sorted set with a value between min and max. - /// When all the elements in a sorted set have the same score, + /// When all the elements in a sorted set have the same score, /// this command forces lexicographical ordering. - /// ZREMRANGEBYLEX: Removes all elements in the sorted set between the + /// ZREMRANGEBYLEX: Removes all elements in the sorted set between the /// lexicographical range specified by min and max. /// /// @@ -718,7 +794,7 @@ private unsafe bool SortedSetLengthByValue(int count, byte* ptr, Sor } /// - /// Increments the score of member in the sorted set stored at key by increment. + /// Increments the score of member in the sorted set stored at key by increment. /// If member does not exist in the sorted set, it is added with increment as its score (as if its previous score was 0.0). /// /// @@ -1082,7 +1158,7 @@ private unsafe bool SortedSetRandomMember(int count, byte* ptr, ref } /// - /// Computes a difference operation between the first and all successive sorted sets + /// Computes a difference operation between the first and all successive sorted sets /// and returns the result to the client. /// The total number of input keys is specified. /// diff --git a/libs/server/Resp/RespCommand.cs b/libs/server/Resp/RespCommand.cs index 726cf1cdcf..5b1af91a4a 100644 --- a/libs/server/Resp/RespCommand.cs +++ b/libs/server/Resp/RespCommand.cs @@ -515,6 +515,10 @@ private RespCommand FastParseCommand(byte* ptr) if (*(long*)ptr == 4702694082085729572L && *(ushort*)(ptr + 8) == 3406 && *(ptr + 10) == 10) return (RespCommand.SortedSet, (byte)SortedSetOperation.ZSCAN); + //[$7|ZMSCORE|] = 13 bytes = 8 (long) + 2 (ushort) + 3 bytes + if (*(long*)ptr == 4851306272719189796L && *(ushort*)(ptr + 8) == 21071 && *(ptr + 10) == 69) + return (RespCommand.SortedSet, (byte)SortedSetOperation.ZMSCORE); + #region SortedSet Operations with Geo Commands //[$6|GEOADD|] = 12 bytes = 8 (long) + 2 (ushort) + 2 bytes if (*(long*)ptr == 4706056307039090212L && *(ushort*)(ptr + 8) == 17476 && *(ptr + 11) == 10) @@ -643,13 +647,13 @@ private RespCommand FastParseCommand(byte* ptr) //[$8|SMEMBERS|] = 14 bytes = 8 (long) + 2 (ushort) + 2 bytes if (*(long*)ptr == 5567941533359749156L && *(ushort*)(ptr + 8) == 17730 && *(ptr + 13) == 10) return (RespCommand.Set, (byte)SetOperation.SMEMBERS); - //[$4|SREM|] = 10 bytes = 8 (long) + 2 (ushort) + //[$4|SREM|] = 10 bytes = 8 (long) + 2 (ushort) if (*(long*)ptr == 5567947030917887012L && *(ushort*)(ptr + 8) == 2573 && *(ptr + 9) == 10) return (RespCommand.Set, (byte)SetOperation.SREM); //[$5|SCARD|] = 11 bytes = 8 (long) + 2 (ushort) + 1 byte if (*(long*)ptr == 5927092608526267684L && *(ushort*)(ptr + 8) == 3396 && *(ptr + 10) == 10) return (RespCommand.Set, (byte)SetOperation.SCARD); - //[$4|SPOP|] = 10 bytes = 8 (long) + 2 (ushort) + //[$4|SPOP|] = 10 bytes = 8 (long) + 2 (ushort) if (*(long*)ptr == 5786932363775521828L && *(ushort*)(ptr + 8) == 2573 && *(ptr + 9) == 10) return (RespCommand.Set, (byte)SetOperation.SPOP); //[$5|SSCAN|] = 14 bytes = 8 (long) + 2 (ushort) diff --git a/libs/server/Resp/RespCommandsInfo.cs b/libs/server/Resp/RespCommandsInfo.cs index 7b118589a6..16d386893c 100644 --- a/libs/server/Resp/RespCommandsInfo.cs +++ b/libs/server/Resp/RespCommandsInfo.cs @@ -138,6 +138,7 @@ public static RespCommandsInfo findCommand(RespCommand cmd, byte subCmd = 0) private static readonly Dictionary sortedSetCommandsInfoMap = new Dictionary { {(byte)SortedSetOperation.ZADD, new RespCommandsInfo("ZADD", RespCommand.SortedSet, -4,null, (byte)SortedSetOperation.ZADD)}, + {(byte)SortedSetOperation.ZMSCORE, new RespCommandsInfo("ZMSCORE", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.ZMSCORE)}, {(byte)SortedSetOperation.ZREM, new RespCommandsInfo("ZREM", RespCommand.SortedSet, -3,null, (byte)SortedSetOperation.ZREM)}, {(byte)SortedSetOperation.ZCARD, new RespCommandsInfo("ZCARD", RespCommand.SortedSet, 2,null, (byte)SortedSetOperation.ZCARD)}, {(byte)SortedSetOperation.ZPOPMAX, new RespCommandsInfo("ZPOPMAX", RespCommand.SortedSet, -2,null, (byte)SortedSetOperation.ZPOPMAX)}, diff --git a/libs/server/Resp/RespInfo.cs b/libs/server/Resp/RespInfo.cs index f4d3117573..c2b59972b8 100644 --- a/libs/server/Resp/RespInfo.cs +++ b/libs/server/Resp/RespInfo.cs @@ -26,7 +26,7 @@ public static HashSet GetCommands() // Checkpointing "SAVE", "LASTSAVE", "BGSAVE", "BGREWRITEAOF", // Sorted Set - "ZADD", "ZCARD", "ZPOPMAX", "ZSCORE", "ZREM", "ZCOUNT", "ZINCRBY", "ZRANK", "ZRANGE", "ZRANGEBYSCORE", "ZREVRANGE", "ZREVRANK", "ZREMRANGEBYLEX", "ZREMRANGEBYRANK", "ZREMRANGEBYSCORE", "ZLEXCOUNT", "ZPOPMIN", "ZRANDMEMBER", "ZDIFF", "ZSCAN", + "ZADD", "ZCARD", "ZPOPMAX", "ZSCORE", "ZREM", "ZCOUNT", "ZINCRBY", "ZRANK", "ZRANGE", "ZRANGEBYSCORE", "ZREVRANGE", "ZREVRANK", "ZREMRANGEBYLEX", "ZREMRANGEBYRANK", "ZREMRANGEBYSCORE", "ZLEXCOUNT", "ZPOPMIN", "ZRANDMEMBER", "ZDIFF", "ZSCAN", "ZMSCORE", // List "LPOP", "LPUSH", "RPOP", "RPUSH", "LLEN", "LTRIM", "LRANGE", "LINDEX", "LINSERT", "LREM", "RPOPLPUSH", "LMOVE", "LPUSHX", "RPUSHX", // Hash diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index f24f5eaaab..5f60beab56 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -162,7 +162,7 @@ public override void Dispose() /// True if the session has been authenticated successfully, false if the user could not be authenticated. bool AuthenticateUser(ReadOnlySpan username, ReadOnlySpan password = default(ReadOnlySpan)) { - // Authenticate user or change to default user if no authentication is supported + // Authenticate user or change to default user if no authentication is supported bool success = _authenticator.CanAuthenticate ? _authenticator.Authenticate(password, username) : true; if (success) @@ -425,6 +425,7 @@ private bool ProcessArrayCommands(ref TGarnetApi storageApi) (RespCommand.SortedSet, (byte)SortedSetOperation.ZCARD) => SortedSetLength(count, ptr, ref storageApi), (RespCommand.SortedSet, (byte)SortedSetOperation.ZPOPMAX) => SortedSetPop(count, ptr, SortedSetOperation.ZPOPMAX, ref storageApi), (RespCommand.SortedSet, (byte)SortedSetOperation.ZSCORE) => SortedSetScore(count, ptr, ref storageApi), + (RespCommand.SortedSet, (byte)SortedSetOperation.ZMSCORE) => SortedSetScores(count, ptr, ref storageApi), (RespCommand.SortedSet, (byte)SortedSetOperation.ZCOUNT) => SortedSetCount(count, ptr, ref storageApi), (RespCommand.SortedSet, (byte)SortedSetOperation.ZINCRBY) => SortedSetIncrement(count, ptr, ref storageApi), (RespCommand.SortedSet, (byte)SortedSetOperation.ZRANK) => SortedSetRank(count, ptr, SortedSetOperation.ZRANK, ref storageApi), diff --git a/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs b/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs index 2144d00614..e67d71102e 100644 --- a/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs +++ b/libs/server/Storage/Session/ObjectStore/SortedSetOps.cs @@ -379,7 +379,7 @@ public unsafe GarnetStatus SortedSetIncrement(ArgSlice key, doub } /// - /// + /// /// /// /// @@ -733,7 +733,21 @@ public GarnetStatus SortedSetScore(byte[] key, ArgSlice input, r => ReadObjectStoreOperationWithOutput(key, input, ref objectStoreContext, ref outputFooter); /// - /// Removes and returns the first element from the sorted set stored at key, + /// Returns the scores of members in the sorted set at key. + /// For every member that does not exist in the sorted set, or if the key does not exist, nil is returned. + /// + /// + /// + /// + /// + /// + /// + public GarnetStatus SortedSetScores(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter, ref TObjectContext objectStoreContext) + where TObjectContext : ITsavoriteContext + => ReadObjectStoreOperationWithOutput(key, input, ref objectStoreContext, ref outputFooter); + + /// + /// Removes and returns the first element from the sorted set stored at key, /// with the scores ordered from low to high (min) or high to low (max). /// /// @@ -775,7 +789,7 @@ public GarnetStatus SortedSetRemoveRangeByLex(byte[] key, ArgSli /// /// Returns the number of elements in the sorted set with a value between min and max. - /// When all the elements in a sorted set have the same score, + /// When all the elements in a sorted set have the same score, /// this command forces lexicographical ordering. /// /// @@ -789,7 +803,7 @@ public GarnetStatus SortedSetLengthByValue(byte[] key, ArgSlice => ReadObjectStoreOperation(key, input, out output, ref objectStoreContext); /// - /// Increments the score of member in the sorted set stored at key by increment. + /// Increments the score of member in the sorted set stored at key by increment. /// If member does not exist in the sorted set, it is added with increment as its score (as if its previous score was 0.0). /// /// diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index abccd8d974..203e16b0e8 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -107,6 +107,7 @@ private int SortedSetObjectKeys(byte subCommand, int inputCount) (byte)SortedSetOperation.ZCARD => SingleKey(1, true, LockType.Shared), (byte)SortedSetOperation.ZPOPMAX => SingleKey(1, true, LockType.Exclusive), (byte)SortedSetOperation.ZSCORE => SingleKey(1, true, LockType.Shared), + (byte)SortedSetOperation.ZMSCORE => SingleKey(1, true, LockType.Shared), (byte)SortedSetOperation.ZCOUNT => SingleKey(1, true, LockType.Shared), (byte)SortedSetOperation.ZINCRBY => SingleKey(1, true, LockType.Exclusive), (byte)SortedSetOperation.ZRANK => SingleKey(1, true, LockType.Exclusive), diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index 1b0f7ae5b0..acf5b35a86 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -341,6 +341,29 @@ public void AddScore() Assert.AreEqual(expectedResponse, actualValue); } + [Test] + public void CanGetMemberScores() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + using var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommands("ZMSCORE nokey", "PING"); + var expectedResponse = "-ERR wrong number of arguments for ZMSCORE command.\r\n+PONG\r\n"; + var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, actualValue); + + var key = "SortedSet_GetMemberScores"; + var added = db.SortedSetAdd(key, entries); + var scores = db.SortedSetScores(key, ["a", "b", "z", "i"]); + Assert.AreEqual(scores, new List() { 1, 2, null, 9 }); + + var memResponse = db.Execute("MEMORY", "USAGE", key); + var memActualValue = ResultType.Integer == memResponse.Type ? Int32.Parse(response.ToString()) : -1; + var memExpectedResponse = 1808; + Assert.AreEqual(memExpectedResponse, memActualValue); + } + [Test] public void CandDoZIncrby() { @@ -470,7 +493,7 @@ public void CanUseZScanWithCollection() // Add some items var r = new Random(); - // Fill a new SortedSetEntry with 1000 random entries + // Fill a new SortedSetEntry with 1000 random entries int n = 1000; var entries = new SortedSetEntry[n]; @@ -550,7 +573,7 @@ public void CanDoZScanWithCursor() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - // Fill a new SortedSetEntry with 1000 random entries + // Fill a new SortedSetEntry with 1000 random entries int n = 1000; var entries = new SortedSetEntry[n]; var keySS = new RedisKey("keySS"); @@ -718,13 +741,13 @@ public void CanDoZRangeByScoreLC(int bytesSent) lightClientRequest.SendCommand("ZADD board 2 two"); lightClientRequest.SendCommand("ZADD board 3 three"); - // 1 < score <= 5 + // 1 < score <= 5 response = lightClientRequest.SendCommandChunks("ZRANGEBYSCORE board (1 5", bytesSent, 3); var expectedResponse = "*2\r\n$3\r\ntwo\r\n$5\r\nthree\r\n"; var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); - // 1 < score <= 5 + // 1 < score <= 5 response = lightClientRequest.SendCommands("ZRANGEBYSCORE board (1 5", "PING", 3, 1); expectedResponse = "*2\r\n$3\r\ntwo\r\n$5\r\nthree\r\n+PONG\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); @@ -777,7 +800,7 @@ public void CanDoZRangeByLex() lightClientRequest.SendCommand("ZADD board 0 f"); lightClientRequest.SendCommand("ZADD board 0 g"); - // get a range by lex order + // get a range by lex order response = lightClientRequest.SendCommand("ZRANGE board (a (d BYLEX", 3); var expectedResponse = "*2\r\n$1\r\nb\r\n$1\r\nc\r\n"; var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); @@ -828,7 +851,7 @@ public void CanDoZRangeByIndexReverse(int bytesSent) lightClientRequest.SendCommand("ZADD board 0 e"); lightClientRequest.SendCommand("ZADD board 0 f"); - // get a range by lex order + // get a range by lex order response = lightClientRequest.SendCommandChunks("ZRANGE board 0 -1 REV", bytesSent, 7); var expectedResponse = "*6\r\n$1\r\nf\r\n$1\r\ne\r\n$1\r\nd\r\n$1\r\nc\r\n$1\r\nb\r\n$1\r\na\r\n"; @@ -1112,7 +1135,7 @@ public void CanDoZRankLC(int bytesSent) [TestCase(100)] public void CanDoZRevRankLC(int bytesSent) { - //ZREVRANK key member + //ZREVRANK key member using var lightClientRequest = TestUtils.CreateRequest(); var response = lightClientRequest.SendCommand("ZADD board 340 Dave"); lightClientRequest.SendCommand("ZADD board 400 Kendra"); @@ -1146,7 +1169,7 @@ public void CanDoZRevRankLC(int bytesSent) [TestCase(100)] public void CanDoZRemRangeByLexLC(int bytesSent) { - //ZREMRANGEBYLEX key member + //ZREMRANGEBYLEX key member using var lightClientRequest = TestUtils.CreateRequest(); var response = lightClientRequest.SendCommand("ZADD myzset 0 aaaa 0 b 0 c 0 d 0 e"); lightClientRequest.SendCommand("ZADD myzset 0 foo 0 zap 0 zip 0 ALPHA 0 alpha"); @@ -1172,7 +1195,7 @@ public void CanDoZRemRangeByLexLC(int bytesSent) [TestCase(100)] public void CanDoZRemRangeByRank(int bytesSent) { - //ZREMRANGEBYRANK key start stop + //ZREMRANGEBYRANK key start stop using var lightClientRequest = TestUtils.CreateRequest(); var response = lightClientRequest.SendCommand("ZADD board 340 Dave"); lightClientRequest.SendCommand("ZADD board 400 Kendra"); @@ -1755,7 +1778,7 @@ public void CanFastForwardExtraArguments() var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); - // Add a large number + // Add a large number response = lightClientRequest.SendCommand("ZADD zset1 -9007199254740992 uno"); expectedResponse = ":1\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); From ae62e711771fbc9d95dba43bc58261b313ba3206 Mon Sep 17 00:00:00 2001 From: ProTip Date: Sun, 24 Mar 2024 16:41:24 -0500 Subject: [PATCH 2/7] Fixup --- test/Garnet.test/RespSortedSetTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index acf5b35a86..d9c50270f4 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -359,7 +359,7 @@ public void CanGetMemberScores() Assert.AreEqual(scores, new List() { 1, 2, null, 9 }); var memResponse = db.Execute("MEMORY", "USAGE", key); - var memActualValue = ResultType.Integer == memResponse.Type ? Int32.Parse(response.ToString()) : -1; + var memActualValue = ResultType.Integer == memResponse.Type ? Int32.Parse(memResponse.ToString()) : -1; var memExpectedResponse = 1808; Assert.AreEqual(memExpectedResponse, memActualValue); } From 68389839d68f3108e001ae2c583392a2ab03c0f1 Mon Sep 17 00:00:00 2001 From: ProTip Date: Sun, 24 Mar 2024 15:25:34 -0500 Subject: [PATCH 3/7] Add vscode created .mono dir to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 93e5482b6c..5e647c958f 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ ClientBin/ *.publishsettings node_modules/ **/log-commits +.mono # RIA/Silverlight projects Generated_Code/ @@ -211,4 +212,4 @@ test/tmp/ # IDE files .vscode/ -.idea/ \ No newline at end of file +.idea/ From 7feced40c4130082da9dd7d93e58e2971fa760fd Mon Sep 17 00:00:00 2001 From: ProTip Date: Mon, 25 Mar 2024 14:47:04 -0500 Subject: [PATCH 4/7] Improve command count check for sortedset scores --- libs/server/Resp/Objects/SortedSetCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index d7eb6799ec..14dfe49efb 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -464,7 +464,7 @@ private unsafe bool SortedSetScores(int count, byte* ptr, ref TGarne ptr += 13; //validation if minimum args - if (count - 2 == 0) + if (count <= 2) { // send error to output return AbortWithWrongNumberOfArguments("ZMSCORE", count); From 411d333b93bc53bcccd133b47987d14f8c253cab Mon Sep 17 00:00:00 2001 From: ProTip Date: Tue, 26 Mar 2024 04:36:06 -0500 Subject: [PATCH 5/7] Address PR feedback --- libs/server/API/IGarnetApi.cs | 4 +- .../Objects/SortedSet/SortedSetObjectImpl.cs | 35 +++++++++-------- libs/server/Resp/CmdStrings.cs | 7 ++-- libs/server/Resp/Objects/SortedSetCommands.cs | 5 ++- libs/server/Resp/RespCommand.cs | 2 +- test/Garnet.test/RespSortedSetTests.cs | 39 +++++++++++++++---- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index db364db328..452200167e 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -987,8 +987,8 @@ public interface IGarnetReadApi GarnetStatus SortedSetScore(byte[] key, ArgSlice input, ref GarnetObjectStoreOutput outputFooter); /// - /// Returns the score of member in the sorted set at key. - /// If member does not exist in the sorted set, or key does not exist, nil is returned. + /// Returns the scores associated with the specified members in the sorted set stored at key. + /// For every member that does not exist in the sorted set, a nil value is returned. /// /// /// diff --git a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs index 20d6fa443c..d9bff58092 100644 --- a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs +++ b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs @@ -175,43 +175,44 @@ private void SortedSetScores(byte* input, int length, ref SpanByteAndMemory outp bool isMemory = false; MemoryHandle ptrHandle = default; - byte* outPtr = output.SpanByte.ToPointer(); - var outCurr = outPtr; - var outEnd = outCurr + output.Length; + byte* outStartPtr = output.SpanByte.ToPointer(); + var outCurrPtr = outStartPtr; + var outEndPtr = outCurrPtr + output.Length; - byte* startptr = input + sizeof(ObjectInputHeader); - byte* inputCurr = startptr; - byte* inputEnd = input + length; + byte* inputStartPtr = input + sizeof(ObjectInputHeader); + byte* inputCurrPtr = inputStartPtr; + byte* inputEndPtr = input + length; try { - while (!RespWriteUtils.WriteArrayLength(count, ref outCurr, outEnd)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + while (!RespWriteUtils.WriteArrayLength(count, ref outCurrPtr, outEndPtr)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); for (int c = 0; c < count; c++) { - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var scoreKey, ref inputCurr, inputEnd)) + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var scoreKey, ref inputCurrPtr, inputEndPtr)) return; if (!sortedSetDict.TryGetValue(scoreKey, out var score)) { - while (!RespWriteUtils.WriteNull(ref outCurr, outEnd)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + while (!RespWriteUtils.WriteNull(ref outCurrPtr, outEndPtr)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); } else { - while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(score.ToString()), ref outCurr, outEnd)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(score.ToString()), ref outCurrPtr, outEndPtr)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); } } - _output.bytesDone = (int)(inputCurr - startptr); + _output.bytesDone = (int)(inputCurrPtr - inputStartPtr); _output.countDone = count; + _output.opsDone = count; } finally { - while (!RespWriteUtils.WriteDirect(ref _output, ref outCurr, outEnd)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outPtr, ref ptrHandle, ref outCurr, ref outEnd); + while (!RespWriteUtils.WriteDirect(ref _output, ref outCurrPtr, outEndPtr)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); if (isMemory) ptrHandle.Dispose(); - output.Length = (int)(outCurr - outPtr); + output.Length = (int)(outCurrPtr - outStartPtr); } } diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index 04e505f915..595151c964 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -20,9 +20,9 @@ public static ReadOnlySpan GetConfig(ReadOnlySpan key) else return RESP_EMPTYLIST; } - /// - /// Request strings - /// + /// + /// Request strings + /// public static ReadOnlySpan CLIENT => "CLIENT"u8; public static ReadOnlySpan SUBSCRIBE => "SUBSCRIBE"u8; public static ReadOnlySpan RUNTXP => "RUNTXP"u8; @@ -85,6 +85,7 @@ public static ReadOnlySpan GetConfig(ReadOnlySpan key) public static ReadOnlySpan RESP_ERR => "-ERR unknown command\r\n"u8; public static ReadOnlySpan RESP_CLUSTER_DISABLED => "-ERR This instance has cluster support disabled\r\n"u8; public static ReadOnlySpan RESP_WRONG_ARGUMENTS => "-ERR wrong number of arguments for 'config|set' command\r\n"u8; + public static ReadOnlySpan RESP_WRONG_TYPE => "-WRONGTYPE Operation against a key holding the wrong kind of value.\r\n"u8; public static ReadOnlySpan RESP_ERRNOTFOUND => "$-1\r\n"u8; public static ReadOnlySpan RESP_ERRNOSUCHKEY => "-ERR no such key\r\n"u8; public static ReadOnlySpan RESP_NOAUTH => "-NOAUTH Authentication required.\r\n"u8; diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index 14dfe49efb..c20463edfe 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -501,7 +501,7 @@ private unsafe bool SortedSetScores(int count, byte* ptr, ref TGarne // Prepare GarnetObjectStore output var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) }; - var status = storageApi.SortedSetScore(key, new ArgSlice((byte*)inputPtr, inputLength), ref outputFooter); + var status = storageApi.SortedSetScores(key, new ArgSlice((byte*)inputPtr, inputLength), ref outputFooter); //restore input *inputPtr = save; @@ -514,8 +514,9 @@ private unsafe bool SortedSetScores(int count, byte* ptr, ref TGarne ptr += objOutputHeader.bytesDone; break; case GarnetStatus.NOTFOUND: - while (!RespWriteUtils.WriteResponse(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + while (!RespWriteUtils.WriteResponse(CmdStrings.RESP_WRONG_TYPE, ref dcurr, dend)) SendAndReset(); + ReadLeftToken(count - 2, ref ptr); break; } } diff --git a/libs/server/Resp/RespCommand.cs b/libs/server/Resp/RespCommand.cs index 5b1af91a4a..777ee2713f 100644 --- a/libs/server/Resp/RespCommand.cs +++ b/libs/server/Resp/RespCommand.cs @@ -516,7 +516,7 @@ private RespCommand FastParseCommand(byte* ptr) return (RespCommand.SortedSet, (byte)SortedSetOperation.ZSCAN); //[$7|ZMSCORE|] = 13 bytes = 8 (long) + 2 (ushort) + 3 bytes - if (*(long*)ptr == 4851306272719189796L && *(ushort*)(ptr + 8) == 21071 && *(ptr + 10) == 69) + if (*(long*)ptr == 4851306272719189796L && *(int*)(ptr + 8) == 222646863 && *(ptr + 12) == 10) return (RespCommand.SortedSet, (byte)SortedSetOperation.ZMSCORE); #region SortedSet Operations with Geo Commands diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index d9c50270f4..508d674cee 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -342,28 +342,53 @@ public void AddScore() } [Test] - public void CanGetMemberScores() + public void CanDoZMScore() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - using var lightClientRequest = TestUtils.CreateRequest(); - var response = lightClientRequest.SendCommands("ZMSCORE nokey", "PING"); - var expectedResponse = "-ERR wrong number of arguments for ZMSCORE command.\r\n+PONG\r\n"; - var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); - Assert.AreEqual(expectedResponse, actualValue); - var key = "SortedSet_GetMemberScores"; var added = db.SortedSetAdd(key, entries); var scores = db.SortedSetScores(key, ["a", "b", "z", "i"]); Assert.AreEqual(scores, new List() { 1, 2, null, 9 }); + Assert.Throws( + () => db.SortedSetScores("nokey", ["a", "b", "c"]), + "WRONGTYPE Operation against a key holding the wrong kind of value."); + + Assert.Throws( + () => db.SortedSetScores("nokey", []), + "ERR wrong number of arguments for ZMSCORE command."); + var memResponse = db.Execute("MEMORY", "USAGE", key); var memActualValue = ResultType.Integer == memResponse.Type ? Int32.Parse(memResponse.ToString()) : -1; var memExpectedResponse = 1808; Assert.AreEqual(memExpectedResponse, memActualValue); } + [Test] + public void CanDoZMScoreLC() + { + using var lightClientRequest = TestUtils.CreateRequest(); + + lightClientRequest.SendCommands("ZADD zmscore 0 a 1 b", "PING"); + + var response = lightClientRequest.SendCommands("ZMSCORE zmscore", "PING"); + var expectedResponse = "-ERR wrong number of arguments for ZMSCORE command.\r\n+PONG\r\n"; + var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommands("ZMSCORE nokey a", "PING"); + expectedResponse = "-WRONGTYPE Operation against a key holding the wrong kind of value.\r\n+PONG\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommands("ZMSCORE zmscore a z b", "PING"); + expectedResponse = "*3\r\n$1\r\n0\r\n$-1\r\n$1\r\n1\r\n+PONG\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, actualValue); + } + [Test] public void CandDoZIncrby() { From 7ea9801ee30088e3a5bf712e49cfd529d5335b7f Mon Sep 17 00:00:00 2001 From: ProTip Date: Wed, 27 Mar 2024 01:15:38 -0500 Subject: [PATCH 6/7] Return nils from ZMSCORE when key is missing --- libs/server/Resp/Objects/SortedSetCommands.cs | 9 +++++++-- test/Garnet.test/RespSortedSetTests.cs | 12 ++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index c20463edfe..f2eba9b2e3 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -514,9 +514,14 @@ private unsafe bool SortedSetScores(int count, byte* ptr, ref TGarne ptr += objOutputHeader.bytesDone; break; case GarnetStatus.NOTFOUND: - while (!RespWriteUtils.WriteResponse(CmdStrings.RESP_WRONG_TYPE, ref dcurr, dend)) + while (!RespWriteUtils.WriteArrayLength(inputCount, ref dcurr, dend)) SendAndReset(); - ReadLeftToken(count - 2, ref ptr); + + for (var c = 0; c < inputCount; c++) + while (!RespWriteUtils.WriteNull(ref dcurr, dend)) + SendAndReset(); + + ReadLeftToken(inputCount, ref ptr); break; } } diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index 508d674cee..9fbef15b87 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -352,9 +352,9 @@ public void CanDoZMScore() var scores = db.SortedSetScores(key, ["a", "b", "z", "i"]); Assert.AreEqual(scores, new List() { 1, 2, null, 9 }); - Assert.Throws( - () => db.SortedSetScores("nokey", ["a", "b", "c"]), - "WRONGTYPE Operation against a key holding the wrong kind of value."); + + scores = db.SortedSetScores("nokey", ["a", "b", "c"]); + Assert.AreEqual(scores, new List() { null, null, null }); Assert.Throws( () => db.SortedSetScores("nokey", []), @@ -378,12 +378,12 @@ public void CanDoZMScoreLC() var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); - response = lightClientRequest.SendCommands("ZMSCORE nokey a", "PING"); - expectedResponse = "-WRONGTYPE Operation against a key holding the wrong kind of value.\r\n+PONG\r\n"; + response = lightClientRequest.SendCommands("ZMSCORE nokey a b c", "PING", 4, 1); + expectedResponse = "*3\r\n$-1\r\n$-1\r\n$-1\r\n+PONG\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); - response = lightClientRequest.SendCommands("ZMSCORE zmscore a z b", "PING"); + response = lightClientRequest.SendCommands("ZMSCORE zmscore a z b", "PING", 4, 1); expectedResponse = "*3\r\n$1\r\n0\r\n$-1\r\n$1\r\n1\r\n+PONG\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); From 3c6bd484196e9696a690628712cbbef2340f93e3 Mon Sep 17 00:00:00 2001 From: ProTip Date: Wed, 27 Mar 2024 01:19:24 -0500 Subject: [PATCH 7/7] Name SortedSetScores pointers based on SortedSetIncrement --- .../Objects/SortedSet/SortedSetObjectImpl.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs index d9bff58092..26b874d9be 100644 --- a/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs +++ b/libs/server/Objects/SortedSet/SortedSetObjectImpl.cs @@ -175,44 +175,45 @@ private void SortedSetScores(byte* input, int length, ref SpanByteAndMemory outp bool isMemory = false; MemoryHandle ptrHandle = default; - byte* outStartPtr = output.SpanByte.ToPointer(); - var outCurrPtr = outStartPtr; - var outEndPtr = outCurrPtr + output.Length; + byte* ptr = output.SpanByte.ToPointer(); + var curr = ptr; + var end = curr + output.Length; + + byte* input_startptr = input + sizeof(ObjectInputHeader); + byte* input_currptr = input_startptr; + byte* input_endptr = input + length; - byte* inputStartPtr = input + sizeof(ObjectInputHeader); - byte* inputCurrPtr = inputStartPtr; - byte* inputEndPtr = input + length; try { - while (!RespWriteUtils.WriteArrayLength(count, ref outCurrPtr, outEndPtr)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); + while (!RespWriteUtils.WriteArrayLength(count, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); for (int c = 0; c < count; c++) { - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var scoreKey, ref inputCurrPtr, inputEndPtr)) + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var scoreKey, ref input_currptr, input_endptr)) return; if (!sortedSetDict.TryGetValue(scoreKey, out var score)) { - while (!RespWriteUtils.WriteNull(ref outCurrPtr, outEndPtr)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); + while (!RespWriteUtils.WriteNull(ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } else { - while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(score.ToString()), ref outCurrPtr, outEndPtr)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); + while (!RespWriteUtils.WriteBulkString(Encoding.ASCII.GetBytes(score.ToString()), ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } } - _output.bytesDone = (int)(inputCurrPtr - inputStartPtr); + _output.bytesDone = (int)(input_currptr - input_startptr); _output.countDone = count; _output.opsDone = count; } finally { - while (!RespWriteUtils.WriteDirect(ref _output, ref outCurrPtr, outEndPtr)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref outStartPtr, ref ptrHandle, ref outCurrPtr, ref outEndPtr); + while (!RespWriteUtils.WriteDirect(ref _output, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); if (isMemory) ptrHandle.Dispose(); - output.Length = (int)(outCurrPtr - outStartPtr); + output.Length = (int)(curr - ptr); } }