diff --git a/libs/server/Objects/SortedSet/SortedSetObject.cs b/libs/server/Objects/SortedSet/SortedSetObject.cs index ba3315d054..008de82622 100644 --- a/libs/server/Objects/SortedSet/SortedSetObject.cs +++ b/libs/server/Objects/SortedSet/SortedSetObject.cs @@ -221,7 +221,7 @@ public override unsafe bool Operate(ref SpanByte input, ref SpanByteAndMemory ou SortedSetRangeByScore(_input, input.Length, ref output); break; case SortedSetOperation.GEOADD: - GeoAdd(_input, input.Length, _output); + GeoAdd(_input, input.Length, ref output); break; case SortedSetOperation.GEOHASH: GeoHash(_input, input.Length, ref output); diff --git a/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs b/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs index ce4ceec7d6..3ffff73789 100644 --- a/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs +++ b/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -46,88 +47,180 @@ private struct GeoSearchOptions public bool WithHash { get; set; } } - private void GeoAdd(byte* input, int length, byte* output) + private void GeoAdd(byte* input, int length, ref SpanByteAndMemory output) { var _input = (ObjectInputHeader*)input; - var _output = (ObjectOutputHeader*)output; - *_output = default; + ObjectOutputHeader _output = default; int count = _input->count; byte* input_startptr = input + sizeof(ObjectInputHeader); byte* input_currptr = input_startptr; - // By default add new elements but do not update the ones already in the set - bool nx = true; + bool isMemory = false; + MemoryHandle ptrHandle = default; + byte* ptr = output.SpanByte.ToPointer(); - bool ch = false; + var curr = ptr; + var end = curr + output.Length; - // Read the options - var optsCount = count % 3; - if (optsCount > 0 && optsCount <= 2) + try { - // Is NX or XX, if not nx then use XX - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var byteOptions, ref input_currptr, input + length)) - return; - nx = (byteOptions.Length == 2 && (byteOptions[0] == (int)'N' && byteOptions[1] == (int)'X') || (byteOptions[0] == (int)'n' && byteOptions[1] == (int)'x')); - if (optsCount == 2) + // By default add new elements but do not update the ones already in the set + var nx = false; + var xx = false; + var ch = false; + + byte* tokenPtr = null; + var tokenSize = 0; + Span tokenSpan = default; + + // Read the options + while (count > 0) { - // Read CH option - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out byteOptions, ref input_currptr, input + length)) + if (!RespReadUtils.ReadPtrWithLengthHeader(ref tokenPtr, ref tokenSize, ref input_currptr, + input + length)) return; - ch = (byteOptions.Length == 2 && (byteOptions[0] == (int)'C' && byteOptions[1] == (int)'H') || (byteOptions[0] == (int)'c' && byteOptions[1] == (int)'h')); + + count--; + tokenSpan = new Span(tokenPtr, tokenSize); + if (tokenSpan.SequenceEqual("NX"u8)) + { + nx = true; + } + else if (tokenSpan.SequenceEqual("XX"u8)) + { + xx = true; + } + else if (tokenSpan.SequenceEqual("CH"u8)) + { + ch = true; + } + else + { + break; + } } - count -= optsCount; - } - int elementsChanged = 0; + ReadOnlySpan errorMessage = default; + // No members defined + if (count == 0) + { + errorMessage = Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrWrongNumArgs, nameof(SortedSetOperation.GEOADD))); + } + // NX & XX can't both be set + // Also, each member definition should contain 3 tokens - longitude latitude member + // Remaining token count should be a multiple of 3 + else if ((nx && xx) || (count + 1) % 3 != 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; + } + else + { + var elementsChanged = 0; - for (int c = 0; c < count / 3; c++) - { - if (!RespReadUtils.ReadDoubleWithLengthHeader(out var longitude, out var parsed, ref input_currptr, input + length)) - return; - if (!RespReadUtils.ReadDoubleWithLengthHeader(out var latitude, out parsed, ref input_currptr, input + length)) - return; - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var member, ref input_currptr, input + length)) - return; + var memberCount = (count + 1) / 3; + for (var c = 0; c < memberCount; c++) + { + double longitude = default; + var longParsed = false; + // If this is the first member, use last token parsed + if (c == 0) + { + longParsed = Utf8Parser.TryParse(tokenSpan, out longitude, out _); + } + else + { + if (!RespReadUtils.ReadDoubleWithLengthHeader(out longitude, out longParsed, ref input_currptr, + input + length)) + return; + count--; + } - if (c < _input->done) - continue; + if (!RespReadUtils.ReadDoubleWithLengthHeader(out var latitude, out var latParsed, ref input_currptr, + input + length)) + return; + count--; - _output->countDone++; + if (!longParsed || !latParsed) + { + errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + break; + } - if (parsed) - { - var score = server.GeoHash.GeoToLongValue(latitude, longitude); - if (score != -1) - { - if (!sortedSetDict.TryGetValue(member, out double scoreStored)) + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var member, ref input_currptr, + input + length)) + return; + count--; + + if (c < _input->done) + continue; + + var score = server.GeoHash.GeoToLongValue(latitude, longitude); + if (score != -1) { - if (nx) + if (!sortedSetDict.TryGetValue(member, out var scoreStored)) { - sortedSetDict.Add(member, score); - sortedSet.Add((score, member)); - _output->opsDone++; + if (!xx) + { + sortedSetDict.Add(member, score); + sortedSet.Add((score, member)); + _output.opsDone++; - this.UpdateSize(member); + this.UpdateSize(member); + elementsChanged++; + } + } + else if (!nx && Math.Abs(scoreStored - score) > double.Epsilon) + { + sortedSetDict[member] = score; + var success = sortedSet.Remove((scoreStored, member)); + Debug.Assert(success); + success = sortedSet.Add((score, member)); + Debug.Assert(success); + elementsChanged++; } } - else if (!nx && scoreStored != score) - { - sortedSetDict[member] = score; - var success = sortedSet.Remove((scoreStored, member)); - Debug.Assert(success); - success = sortedSet.Add((score, member)); - Debug.Assert(success); - elementsChanged++; - } + } + + _output.opsDone = ch ? elementsChanged : _output.opsDone; + if (elementsChanged == 0) + { + } } - _output->opsDone = ch ? elementsChanged : _output->opsDone; + + // Flush unread tokens + while (count > 0) + { + RespReadUtils.ReadPtrWithLengthHeader(ref tokenPtr, ref tokenSize, ref input_currptr, + input + length); + count--; + } + + if (errorMessage != default) + { + while (!RespWriteUtils.WriteError(errorMessage, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + } + + if (_output.opsDone == 0) + { + + } + + // Write output + _output.bytesDone = (int)(input_currptr - input_startptr); + _output.countDone = _input->count - count; } + finally + { + while (!RespWriteUtils.WriteDirect(ref _output, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - // Write output - _output->bytesDone = (int)(input_currptr - input_startptr); + if (isMemory) ptrHandle.Dispose(); + output.Length = (int)(curr - ptr); + } } private void GeoHash(byte* input, int length, ref SpanByteAndMemory output) diff --git a/libs/server/Resp/ArrayCommands.cs b/libs/server/Resp/ArrayCommands.cs index d430740138..493f2b1e5c 100644 --- a/libs/server/Resp/ArrayCommands.cs +++ b/libs/server/Resp/ArrayCommands.cs @@ -861,15 +861,8 @@ private bool NetworkTYPE(int count, byte* ptr, ref TGarnetApi storag private bool NetworkMODULE(int count, byte* ptr, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - if (count != 1) - return AbortWithWrongNumberOfArguments("MODULE", count); - - // MODULE nameofmodule - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var nameofmodule, ref ptr, recvBufferPtr + bytesRead)) - return false; - // TODO: pending implementation for module support. - while (!RespWriteUtils.WriteEmptyArray(ref dcurr, dend)) + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) SendAndReset(); // Advance pointers diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index 60395de62e..a9d50746bb 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -82,6 +82,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_RETURN_VAL_1 => ":1\r\n"u8; public static ReadOnlySpan RESP_RETURN_VAL_0 => ":0\r\n"u8; public static ReadOnlySpan RESP_RETURN_VAL_N1 => ":-1\r\n"u8; + public static ReadOnlySpan RESP_RETURN_VAL_N2 => ":-2\r\n"u8; public static ReadOnlySpan SUSCRIBE_PONG => "*2\r\n$4\r\npong\r\n$0\r\n\r\n"u8; public static ReadOnlySpan RESP_PONG => "+PONG\r\n"u8; public static ReadOnlySpan RESP_EMPTY => "$0\r\n\r\n"u8; @@ -123,6 +124,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_GENERIC_SELECT_INVALID_INDEX => "ERR invalid database index."u8; public static ReadOnlySpan RESP_ERR_GENERIC_SELECT_CLUSTER_MODE => "ERR SELECT is not allowed in cluster mode"u8; public static ReadOnlySpan RESP_ERR_UNSUPPORTED_PROTOCOL_VERSION => "ERR Unsupported protocol version"u8; + public static ReadOnlySpan RESP_ERR_NOT_VALID_FLOAT => "ERR value is not a valid float"u8; public static ReadOnlySpan RESP_WRONGPASS_INVALID_PASSWORD => "WRONGPASS Invalid password"u8; public static ReadOnlySpan RESP_WRONGPASS_INVALID_USERNAME_PASSWORD => "WRONGPASS Invalid username/password combination"u8; @@ -133,6 +135,8 @@ static partial class CmdStrings public const string GenericErrWrongNumArgs = "ERR wrong number of arguments for '{0}' command"; public const string GenericErrUnknownOption = "ERR Unknown option or number of arguments for CONFIG SET - '{0}'"; public const string GenericErrUnknownSubCommand = "ERR unknown subcommand '{0}'. Try {1} HELP"; + public const string GenericErrWrongNumArgsTxn = + "ERR Invalid number of parameters to stored proc {0}, expected {1}, actual {2}"; /// /// Object types diff --git a/libs/server/Resp/KeyAdminCommands.cs b/libs/server/Resp/KeyAdminCommands.cs index f45a9f27a1..f757626b3f 100644 --- a/libs/server/Resp/KeyAdminCommands.cs +++ b/libs/server/Resp/KeyAdminCommands.cs @@ -347,7 +347,7 @@ private bool NetworkTTL(byte* ptr, RespCommand command, ref TGarnetA } else { - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_N1, ref dcurr, dend)) + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_RETURN_VAL_N2, ref dcurr, dend)) SendAndReset(); } return true; diff --git a/libs/server/Resp/Objects/SetCommands.cs b/libs/server/Resp/Objects/SetCommands.cs index 0c18a56736..6e273539e6 100644 --- a/libs/server/Resp/Objects/SetCommands.cs +++ b/libs/server/Resp/Objects/SetCommands.cs @@ -917,8 +917,16 @@ private unsafe bool SetRandomMember(int count, byte* ptr, ref TGarne return false; break; case GarnetStatus.NOTFOUND: + if (count == 2) + { + while (!RespWriteUtils.WriteEmptyArray(ref dcurr, dend)) + SendAndReset(); + break; + } + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); + break; } diff --git a/libs/server/Resp/Objects/SharedObjectCommands.cs b/libs/server/Resp/Objects/SharedObjectCommands.cs index f3cf493049..8c46eaf8d4 100644 --- a/libs/server/Resp/Objects/SharedObjectCommands.cs +++ b/libs/server/Resp/Objects/SharedObjectCommands.cs @@ -17,6 +17,7 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase /// Number of tokens in the buffer, including the name of the command /// Pointer to the inpu buffer /// SortedSet, Hash or Set type + /// The RESP command called /// The storageAPI object /// private unsafe bool ObjectScan(int count, byte* ptr, GarnetObjectType objectType, ref TGarnetApi storageApi) @@ -25,109 +26,114 @@ private unsafe bool ObjectScan(int count, byte* ptr, GarnetObjectTyp // Check number of required parameters if (count < 2) { - // Forward tokens in the input - ReadLeftToken(count, ref ptr); + var cmdName = objectType switch + { + GarnetObjectType.Hash => nameof(HashOperation.HSCAN), + GarnetObjectType.Set => nameof(SetOperation.SSCAN), + GarnetObjectType.SortedSet => nameof(SortedSetOperation.ZSCAN), + GarnetObjectType.All => nameof(RespCommand.COSCAN), + _ => nameof(RespCommand.NONE) + }; + + return AbortWithWrongNumberOfArguments(cmdName, count); } - else + + // Read key for the scan + if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, recvBufferPtr + bytesRead)) + return false; + + // Get cursor value + if (!RespReadUtils.ReadStringWithLengthHeader(out var cursor, ref ptr, recvBufferPtr + bytesRead)) + return false; + + if (!Int32.TryParse(cursor, out int cursorValue) || cursorValue < 0) { - // Read key for the scan - if (!RespReadUtils.ReadByteArrayWithLengthHeader(out var key, ref ptr, recvBufferPtr + bytesRead)) - return false; + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_CURSORVALUE, ref dcurr, dend)) + SendAndReset(); + ReadLeftToken(count - 1, ref ptr); + return true; + } - // Get cursor value - if (!RespReadUtils.ReadStringWithLengthHeader(out var cursor, ref ptr, recvBufferPtr + bytesRead)) + if (NetworkSingleKeySlotVerify(key, false)) + { + var bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); + if (!DrainCommands(bufSpan, count)) return false; + return true; + } - if (!Int32.TryParse(cursor, out int cursorValue) || cursorValue < 0) - { - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_CURSORVALUE, ref dcurr, dend)) - SendAndReset(); - ReadLeftToken(count - 1, ref ptr); - return true; - } + // Prepare input + // Header + size of int for the limitCountInOutput + var inputPtr = (ObjectInputHeader*)(ptr - ObjectInputHeader.Size - sizeof(int)); + var ptrToInt = (int*)(ptr - sizeof(int)); - if (NetworkSingleKeySlotVerify(key, false)) - { - var bufSpan = new ReadOnlySpan(recvBufferPtr, bytesRead); - if (!DrainCommands(bufSpan, count)) - return false; - return true; - } + // Save old values on buffer for possible revert + var save = *inputPtr; + var savePtrToInt = *ptrToInt; - // Prepare input - // Header + size of int for the limitCountInOutput - var inputPtr = (ObjectInputHeader*)(ptr - ObjectInputHeader.Size - sizeof(int)); - var ptrToInt = (int*)(ptr - sizeof(int)); + // Build the input + byte* pcurr = (byte*)inputPtr; - // Save old values on buffer for possible revert - var save = *inputPtr; - var savePtrToInt = *ptrToInt; + // ObjectInputHeader + (*(ObjectInputHeader*)(pcurr)).header.type = objectType; + (*(ObjectInputHeader*)(pcurr)).header.flags = 0; - // Build the input - byte* pcurr = (byte*)inputPtr; + switch (objectType) + { + case GarnetObjectType.Hash: + (*(ObjectInputHeader*)(pcurr)).header.HashOp = HashOperation.HSCAN; + break; + case GarnetObjectType.Set: + (*(ObjectInputHeader*)(pcurr)).header.SetOp = SetOperation.SSCAN; + break; + case GarnetObjectType.SortedSet: + (*(ObjectInputHeader*)(pcurr)).header.SortedSetOp = SortedSetOperation.ZSCAN; + break; + case GarnetObjectType.All: + (*(ObjectInputHeader*)(pcurr)).header.cmd = RespCommand.COSCAN; + break; + } - // ObjectInputHeader - (*(ObjectInputHeader*)(pcurr)).header.type = objectType; - (*(ObjectInputHeader*)(pcurr)).header.flags = 0; + // Tokens already processed: 3, command, key and cursor + (*(ObjectInputHeader*)(pcurr)).count = count - 2; - switch (objectType) - { - case GarnetObjectType.Hash: - (*(ObjectInputHeader*)(pcurr)).header.HashOp = HashOperation.HSCAN; - break; - case GarnetObjectType.Set: - (*(ObjectInputHeader*)(pcurr)).header.SetOp = SetOperation.SSCAN; - break; - case GarnetObjectType.SortedSet: - (*(ObjectInputHeader*)(pcurr)).header.SortedSetOp = SortedSetOperation.ZSCAN; - break; - case GarnetObjectType.All: - (*(ObjectInputHeader*)(pcurr)).header.cmd = RespCommand.COSCAN; - break; - } - - // Tokens already processed: 3, command, key and cursor - (*(ObjectInputHeader*)(pcurr)).count = count - 2; - - // Cursor value - (*(ObjectInputHeader*)(pcurr)).done = cursorValue; - pcurr += ObjectInputHeader.Size; - - // Object Input Limit - *(int*)pcurr = storeWrapper.serverOptions.ObjectScanCountLimit; - pcurr += sizeof(int); - - // Prepare length of header in input buffer - var inputLength = (int)(recvBufferPtr + bytesRead - (byte*)inputPtr); - - // Prepare GarnetObjectStore output - var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) }; - var status = storageApi.ObjectScan(key, new ArgSlice((byte*)inputPtr, inputLength), ref outputFooter); - - //restore input buffer - *inputPtr = save; - *ptrToInt = savePtrToInt; - - switch (status) - { - case GarnetStatus.OK: - // Process output - var objOutputHeader = ProcessOutputWithHeader(outputFooter.spanByteAndMemory); - // Validation for partial input reading or error - if (objOutputHeader.countDone == Int32.MinValue) - return false; - ptr += objOutputHeader.bytesDone; - break; - case GarnetStatus.NOTFOUND: - while (!RespWriteUtils.WriteScanOutputHeader(0, ref dcurr, dend)) - SendAndReset(); - while (!RespWriteUtils.WriteEmptyArray(ref dcurr, dend)) - SendAndReset(); - // Fast forward left of the input - ReadLeftToken(count - 2, ref ptr); - break; - } + // Cursor value + (*(ObjectInputHeader*)(pcurr)).done = cursorValue; + pcurr += ObjectInputHeader.Size; + + // Object Input Limit + *(int*)pcurr = storeWrapper.serverOptions.ObjectScanCountLimit; + pcurr += sizeof(int); + + // Prepare length of header in input buffer + var inputLength = (int)(recvBufferPtr + bytesRead - (byte*)inputPtr); + // Prepare GarnetObjectStore output + var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) }; + var status = storageApi.ObjectScan(key, new ArgSlice((byte*)inputPtr, inputLength), ref outputFooter); + + //restore input buffer + *inputPtr = save; + *ptrToInt = savePtrToInt; + + switch (status) + { + case GarnetStatus.OK: + // Process output + var objOutputHeader = ProcessOutputWithHeader(outputFooter.spanByteAndMemory); + // Validation for partial input reading or error + if (objOutputHeader.countDone == Int32.MinValue) + return false; + ptr += objOutputHeader.bytesDone; + break; + case GarnetStatus.NOTFOUND: + while (!RespWriteUtils.WriteScanOutputHeader(0, ref dcurr, dend)) + SendAndReset(); + while (!RespWriteUtils.WriteEmptyArray(ref dcurr, dend)) + SendAndReset(); + // Fast forward left of the input + ReadLeftToken(count - 2, ref ptr); + break; } // Update read pointer diff --git a/libs/server/Transaction/TxnRespCommands.cs b/libs/server/Transaction/TxnRespCommands.cs index b319a96c65..e7fcbdb19c 100644 --- a/libs/server/Transaction/TxnRespCommands.cs +++ b/libs/server/Transaction/TxnRespCommands.cs @@ -250,6 +250,9 @@ private bool NetworkRUNTXPFast(byte* ptr) private bool NetworkRUNTXP(int count, byte* ptr) { + if (count < 1) + return AbortWithWrongNumberOfArguments(nameof(RespCommand.RUNTXP), count); + if (!RespReadUtils.ReadIntWithLengthHeader(out int txid, ref ptr, recvBufferPtr + bytesRead)) return false; @@ -275,7 +278,9 @@ private bool NetworkRUNTXP(int count, byte* ptr) } else { - while (!RespWriteUtils.WriteError($"ERR Invalid number of parameters to stored proc {txid}, expected {numParams}, actual {count - 1}", ref dcurr, dend)) + while (!RespWriteUtils.WriteError( + string.Format(CmdStrings.GenericErrWrongNumArgsTxn, txid, numParams, count - 1), ref dcurr, + dend)) SendAndReset(); return true; } diff --git a/test/Garnet.test/RespHashTests.cs b/test/Garnet.test/RespHashTests.cs index 85e8d5daf3..d141b9e75c 100644 --- a/test/Garnet.test/RespHashTests.cs +++ b/test/Garnet.test/RespHashTests.cs @@ -298,6 +298,18 @@ public void CanDoHashScan() db.HashSet("user:user1", [new HashEntry("name", "Alice"), new HashEntry("email", "email@example.com"), new HashEntry("age", "30")]); + // HSCAN without key + try + { + db.Execute("HSCAN"); + Assert.Fail(); + } + catch (RedisServerException e) + { + var expectedErrorMessage = string.Format(CmdStrings.GenericErrWrongNumArgs, nameof(HashOperation.HSCAN)); + Assert.AreEqual(expectedErrorMessage, e.Message); + } + // HSCAN without parameters members = db.HashScan("user:user1"); Assert.IsTrue(((IScanningCursor)members).Cursor == 0); diff --git a/test/Garnet.test/RespSetTest.cs b/test/Garnet.test/RespSetTest.cs index 945c2225b7..cca9b728e5 100644 --- a/test/Garnet.test/RespSetTest.cs +++ b/test/Garnet.test/RespSetTest.cs @@ -194,6 +194,18 @@ public void CanUseSScanNoParameters() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); + // SSCAN without key + try + { + db.Execute("SSCAN"); + Assert.Fail(); + } + catch (RedisServerException e) + { + var expectedErrorMessage = string.Format(CmdStrings.GenericErrWrongNumArgs, nameof(Garnet.server.SetOperation.SSCAN)); + Assert.AreEqual(expectedErrorMessage, e.Message); + } + // Use setscan on non existing key var items = db.SetScan(new RedisKey("foo"), new RedisValue("*"), pageSize: 10); Assert.IsEmpty(items, "Failed to use SetScan on non existing key"); @@ -707,48 +719,58 @@ public void CanDoSRANDMEMBERWithCountCommandLC() { var myset = new HashSet { "one", "two", "three", "four", "five" }; + // Check SRANDMEMBER with non-existing key + using var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommand("SRANDMEMBER myset"); + var expectedResponse = "$-1\r\n"; + var strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, strResponse); + + // Check SRANDMEMBER with non-existing key and count + response = lightClientRequest.SendCommand("SRANDMEMBER myset 3"); + expectedResponse = "*0\r\n"; + strResponse = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, strResponse); + CreateLongSet(); - using (var lightClientRequest = TestUtils.CreateRequest()) + response = lightClientRequest.SendCommand("SRANDMEMBER myset", 1); + var strLen = Encoding.ASCII.GetString(response).Substring(1, 1); + var item = Encoding.ASCII.GetString(response).Substring(4, Int32.Parse(strLen)); + Assert.IsTrue(myset.Contains(item)); + + // Get three random members + response = lightClientRequest.SendCommand("SRANDMEMBER myset 3", 3); + strResponse = Encoding.ASCII.GetString(response); + Assert.AreEqual('*', strResponse[0]); + + var arrLenEndIdx = strResponse.IndexOf("\r\n", StringComparison.InvariantCultureIgnoreCase); + Assert.IsTrue(arrLenEndIdx > 1); + + var strArrLen = Encoding.ASCII.GetString(response).Substring(1, arrLenEndIdx - 1); + Assert.IsTrue(int.TryParse(strArrLen, out var arrLen)); + Assert.AreEqual(3, arrLen); + + // Get 6 random members and verify that at least two elements are the same + response = lightClientRequest.SendCommand("SRANDMEMBER myset -6", 6); + arrLenEndIdx = Encoding.ASCII.GetString(response).IndexOf("\r\n", StringComparison.InvariantCultureIgnoreCase); + strArrLen = Encoding.ASCII.GetString(response).Substring(1, arrLenEndIdx - 1); + Assert.IsTrue(int.TryParse(strArrLen, out arrLen)); + + var members = new HashSet(); + var repeatedMembers = false; + for (int i = 0; i < arrLen; i++) { - var response = lightClientRequest.SendCommand("SRANDMEMBER myset", 1); - var strLen = Encoding.ASCII.GetString(response).Substring(1, 1); - var item = Encoding.ASCII.GetString(response).Substring(4, Int32.Parse(strLen)); - Assert.IsTrue(myset.Contains(item)); - - // Get three random members - response = lightClientRequest.SendCommand("SRANDMEMBER myset 3", 3); - var strResponse = Encoding.ASCII.GetString(response); - Assert.AreEqual('*', strResponse[0]); - - var arrLenEndIdx = strResponse.IndexOf("\r\n", StringComparison.InvariantCultureIgnoreCase); - Assert.IsTrue(arrLenEndIdx > 1); - - var strArrLen = Encoding.ASCII.GetString(response).Substring(1, arrLenEndIdx - 1); - Assert.IsTrue(int.TryParse(strArrLen, out var arrLen)); - Assert.AreEqual(3, arrLen); - - // Get 6 random members and verify that at least two elements are the same - response = lightClientRequest.SendCommand("SRANDMEMBER myset -6", 6); - arrLenEndIdx = Encoding.ASCII.GetString(response).IndexOf("\r\n", StringComparison.InvariantCultureIgnoreCase); - strArrLen = Encoding.ASCII.GetString(response).Substring(1, arrLenEndIdx - 1); - Assert.IsTrue(int.TryParse(strArrLen, out arrLen)); - - var members = new HashSet(); - var repeatedMembers = false; - for (int i = 0; i < arrLen; i++) + var member = Encoding.ASCII.GetString(response).Substring(arrLenEndIdx + 2, response.Length - arrLenEndIdx - 5); + if (members.Contains(member)) { - var member = Encoding.ASCII.GetString(response).Substring(arrLenEndIdx + 2, response.Length - arrLenEndIdx - 5); - if (members.Contains(member)) - { - repeatedMembers = true; - break; - } - members.Add(member); + repeatedMembers = true; + break; } - - Assert.IsTrue(repeatedMembers, "At least two members are repeated."); + members.Add(member); } + + Assert.IsTrue(repeatedMembers, "At least two members are repeated."); } [Test] diff --git a/test/Garnet.test/RespSortedSetGeoTests.cs b/test/Garnet.test/RespSortedSetGeoTests.cs index e0ba908262..56bea8d9b6 100644 --- a/test/Garnet.test/RespSortedSetGeoTests.cs +++ b/test/Garnet.test/RespSortedSetGeoTests.cs @@ -3,7 +3,6 @@ using System; using System.Globalization; -using System.IO; using System.Text; using Garnet.server; using NUnit.Framework; @@ -283,13 +282,32 @@ public void CanUseGeoSearchWithCities(int bytesSent) public void CanDoGeoAddWhenInvalidPairLC(int bytesSent) { using var lightClientRequest = TestUtils.CreateRequest(); - var response = lightClientRequest.SendCommandChunks("GEOADD Sicily NX 13.361389 38.115556 Palermo 15.087269 37.502669 Catania", bytesSent); - var expectedResponse = ":2\r\n"; + + // Check GEOADD without members + var response = lightClientRequest.SendCommandChunks("GEOADD Sicily NX", bytesSent); + var expectedResponse = $"-{string.Format(CmdStrings.GenericErrWrongNumArgs, nameof(SortedSetOperation.GEOADD))}\r\n"; var actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); + // Check GEOADD with insufficient parameters + //response = lightClientRequest.SendCommandChunks("GEOADD Sicily NX CH 13.361389 38.115556", bytesSent); + //expectedResponse = $"-{Encoding.ASCII.GetString(CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR)}\r\n"; + //actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + //Assert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommandChunks("GEOADD Sicily NX 13.361389 38.115556 Palermo 15.087269 37.502669 Catania", bytesSent); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, actualValue); + + // Add new elements, return only the elements changed + response = lightClientRequest.SendCommandChunks("GEOADD Sicily NX CH 14.361389 39.115556 Palermo 15.087269 37.502669 Catania 38.0350 14.0212 Cefalu 37.8545 15.2889 Taormina", bytesSent); + expectedResponse = ":2\r\n"; + actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); + Assert.AreEqual(expectedResponse, actualValue); + // Only update elements, return only the elements changed - response = lightClientRequest.SendCommandChunks("GEOADD Sicily XX CH 14.361389 39.115556 Palermo 15.087269 37.502669 Catania", bytesSent); + response = lightClientRequest.SendCommandChunks("GEOADD Sicily XX CH 15.361389 39.115556 Palermo 15.087269 37.502669 Catania", bytesSent); expectedResponse = ":1\r\n"; actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); Assert.AreEqual(expectedResponse, actualValue); diff --git a/test/Garnet.test/RespSortedSetTests.cs b/test/Garnet.test/RespSortedSetTests.cs index 5a0ce365c6..2816a28c73 100644 --- a/test/Garnet.test/RespSortedSetTests.cs +++ b/test/Garnet.test/RespSortedSetTests.cs @@ -476,6 +476,18 @@ public void CanUseZScanNoParameters() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); + // ZSCAN without key + try + { + db.Execute("ZSCAN"); + Assert.Fail(); + } + catch (RedisServerException e) + { + var expectedErrorMessage = string.Format(CmdStrings.GenericErrWrongNumArgs, nameof(SortedSetOperation.ZSCAN)); + Assert.AreEqual(expectedErrorMessage, e.Message); + } + // Use sortedsetscan on non existing key var items = db.SortedSetScan(new RedisKey("foo"), new RedisValue("*"), pageSize: 10); Assert.IsEmpty(items, "Failed to use SortedSetScan on non existing key"); diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index c267604320..dc49d4ba62 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -1220,7 +1220,13 @@ public void PersistTTLTest() var val = "expireValue"; var expire = 2; + var ttl = db.Execute("TTL", key); + Assert.AreEqual(-2, (int)ttl); + db.StringSet(key, val); + ttl = db.Execute("TTL", key); + Assert.AreEqual(-1, (int)ttl); + db.KeyExpire(key, TimeSpan.FromSeconds(expire)); var time = db.KeyTimeToLive(key); @@ -1247,7 +1253,13 @@ public void ObjectTTLTest() var key = "expireKey"; var expire = 2; + var ttl = db.Execute("TTL", key); + Assert.AreEqual(-2, (int)ttl); + db.SortedSetAdd(key, key, 1.0); + ttl = db.Execute("TTL", key); + Assert.AreEqual(-1, (int)ttl); + db.KeyExpire(key, TimeSpan.FromSeconds(expire)); var time = db.KeyTimeToLive(key); @@ -1770,11 +1782,17 @@ public void TTLTestMilliseconds() var val = "myKeyValue"; var expireTimeInMilliseconds = 3000; + var pttl = db.Execute("PTTL", key); + Assert.AreEqual(-2, (int)pttl); + db.StringSet(key, val); + pttl = db.Execute("PTTL", key); + Assert.AreEqual(-1, (int)pttl); + db.KeyExpire(key, TimeSpan.FromMilliseconds(expireTimeInMilliseconds)); //check TTL of the key in milliseconds - var pttl = db.Execute("PTTL", key); + pttl = db.Execute("PTTL", key); Assert.IsTrue(long.TryParse(pttl.ToString(), out var pttlInMs)); Assert.IsTrue(pttlInMs > 0); diff --git a/test/Garnet.test/RespTransactionProcTests.cs b/test/Garnet.test/RespTransactionProcTests.cs index b82eb69f0a..6d5798bac1 100644 --- a/test/Garnet.test/RespTransactionProcTests.cs +++ b/test/Garnet.test/RespTransactionProcTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Threading; +using Garnet.server; using NUnit.Framework; using StackExchange.Redis; @@ -60,11 +61,24 @@ public void TransactionProcTest1() public void TransactionProcTest2() { // Register sample custom command (SETIFPM = "set if prefix match") - int id = server.Register.NewTransactionProc("READWRITETX", 3, () => new ReadWriteTxn()); + var numParams = 3; + var id = server.Register.NewTransactionProc("READWRITETX", numParams, () => new ReadWriteTxn()); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); + // Check RUNTXP without id + try + { + db.Execute("RUNTXP"); + Assert.Fail(); + } + catch (RedisServerException e) + { + var expectedErrorMessage = string.Format(CmdStrings.GenericErrWrongNumArgs, nameof(RespCommand.RUNTXP)); + Assert.AreEqual(expectedErrorMessage, e.Message); + } + string readkey = "readkey"; string value = "foovalue0"; db.StringSet(readkey, value); @@ -72,8 +86,19 @@ public void TransactionProcTest2() string writekey1 = "writekey1"; string writekey2 = "writekey2"; - var result = db.Execute("RUNTXP", id, readkey, writekey1, writekey2); + // Check RUNTXP with insufficient parameters + try + { + db.Execute("RUNTXP", id, readkey); + Assert.Fail(); + } + catch (RedisServerException e) + { + var expectedErrorMessage = string.Format(CmdStrings.GenericErrWrongNumArgsTxn, id, numParams, 1); + Assert.AreEqual(expectedErrorMessage, e.Message); + } + var result = db.Execute("RUNTXP", id, readkey, writekey1, writekey2); Assert.AreEqual("SUCCESS", (string)result); // Read keys to verify transaction succeeded