From 508cb38292117046a556c2414aac596dce29427a Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Wed, 2 Oct 2024 23:24:18 +0530 Subject: [PATCH 1/7] Added INCRBYFLOAT command --- libs/common/NumUtils.cs | 146 ++++++++++++++++++ libs/server/Resp/BasicCommands.cs | 38 +++-- libs/server/Resp/Parser/RespCommand.cs | 5 + libs/server/Resp/RespCommandsInfo.json | 29 ++++ 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 + test/Garnet.test/RespTests.cs | 71 +++++++++ website/docs/commands/api-compatibility.md | 2 +- website/docs/commands/raw-string.md | 16 ++ 13 files changed, 469 insertions(+), 10 deletions(-) diff --git a/libs/common/NumUtils.cs b/libs/common/NumUtils.cs index 0f63146b60..360e31f523 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,80 @@ 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 into sequence of ASCII bytes + /// + /// Double value to convert + /// Number of digits in 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 +217,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 +484,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/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index c2cf817ad6..f43c3adbe6 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -723,13 +723,14 @@ private bool NetworkIncrement(RespCommand cmd, ref TGarnetApi storag where TGarnetApi : IGarnetApi { Debug.Assert(cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY || cmd == RespCommand.INCR || - cmd == RespCommand.DECR); + cmd == RespCommand.DECR || cmd == RespCommand.INCRBYFLOAT); var key = parseState.GetArgSliceByRef(0); var sbKey = key.SpanByte; ArgSlice input = default; - if (cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY) + var isFloat = cmd == RespCommand.INCRBYFLOAT; + if (cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY || isFloat) { // Parse value argument // NOTE: Parse empty strings for better error messages through storageApi.Increment @@ -760,24 +761,43 @@ private bool NetworkIncrement(RespCommand cmd, ref TGarnetApi storag input = new ArgSlice(valPtr, vSize); } - Span outputBuffer = stackalloc byte[NumUtils.MaximumFormatInt64Length + 1]; + Span outputBuffer = isFloat ? stackalloc byte[NumUtils.MaximumFormatDoubleLength + 1] : stackalloc byte[NumUtils.MaximumFormatInt64Length + 1]; 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 == (isFloat ? NumUtils.MaximumFormatDoubleLength : NumUtils.MaximumFormatInt64Length) + 1 ? (OperationError)output.Span[0] : OperationError.SUCCESS; switch (errorFlag) { case OperationError.SUCCESS: - while (!RespWriteUtils.WriteIntegerFromBytes(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) - SendAndReset(); + if (isFloat) + { + while (!RespWriteUtils.WriteBulkString(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) + SendAndReset(); + } + else + { + while (!RespWriteUtils.WriteIntegerFromBytes(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) + SendAndReset(); + } break; case OperationError.INVALID_TYPE: - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, - dend)) - SendAndReset(); + if (isFloat) + { + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_NOT_VALID_FLOAT, ref dcurr, + dend)) + SendAndReset(); + } + else + { + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, + dend)) + SendAndReset(); + } break; default: throw new GarnetException($"Invalid OperationError {errorFlag}"); diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index ab36128081..7eecb17e21 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -97,6 +97,7 @@ public enum RespCommand : byte HSETNX, INCR, INCRBY, + INCRBYFLOAT, LINSERT, LMOVE, LMPOP, @@ -1346,6 +1347,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SINTERSTORE; } + 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/RespCommandsInfo.json b/libs/server/Resp/RespCommandsInfo.json index aed450d1d1..43991d746b 100644 --- a/libs/server/Resp/RespCommandsInfo.json +++ b/libs/server/Resp/RespCommandsInfo.json @@ -2463,6 +2463,35 @@ ], "SubCommands": null }, + { + "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/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index d611922a88..268d2ed364 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -526,6 +526,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 => NetworkIncrement(RespCommand.INCRBYFLOAT, 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 c24563ef50..46ba36bee6 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -377,6 +377,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 @@ -396,6 +414,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()); @@ -403,6 +438,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. /// @@ -438,6 +480,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 /// @@ -468,6 +541,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 be90499b1d..17aad48663 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 1e00233072..e49b3e3549 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -143,6 +143,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/RespTests.cs b/test/Garnet.test/RespTests.cs index ac5a60fff8..7c6eae46e7 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading; @@ -862,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 31eec09a7c..3a24d0c0c5 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 267e5d37afa62e5aa0d9b90ac77771d9b0798339 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Wed, 2 Oct 2024 23:52:23 +0530 Subject: [PATCH 2/7] Fixed code format issue --- libs/common/NumUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/NumUtils.cs b/libs/common/NumUtils.cs index 360e31f523..5edc0beeb6 100644 --- a/libs/common/NumUtils.cs +++ b/libs/common/NumUtils.cs @@ -499,7 +499,7 @@ public static int NumOfCharInDouble(double v, out int integerDigits, out byte si return 1; } - Debug.Assert(!double.IsNaN(v) && !double.IsInfinity(v)); + 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); From 383f46d661544cc724b595869aca22b1697e4dfe Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Thu, 3 Oct 2024 00:37:53 +0530 Subject: [PATCH 3/7] Added ACL test --- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 8c755b0074..25d186d482 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -3288,6 +3288,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() { From 1c60249673281e884bc2573d8389905cd0a1d66d Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Mon, 7 Oct 2024 01:15:37 +0530 Subject: [PATCH 4/7] Fixed the warning --- libs/common/NumUtils.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/common/NumUtils.cs b/libs/common/NumUtils.cs index 5edc0beeb6..98433df301 100644 --- a/libs/common/NumUtils.cs +++ b/libs/common/NumUtils.cs @@ -98,10 +98,11 @@ public static int DoubleToSpanByte(double value, Span dest) } /// - /// Convert double into sequence of ASCII bytes + /// Convert double number into sequence of ASCII bytes /// - /// Double value to convert - /// Number of digits in value + /// 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) { From 0164bb2dc84d1088f99eb824921cabf9c993673e Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Wed, 9 Oct 2024 12:46:00 +0530 Subject: [PATCH 5/7] Removed unused import --- test/Garnet.test/RespTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 1cba497bcd..adfdba4ef2 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -3,10 +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; From 7b9cdf3a3b4b28909ada9dd60e1272144cb1cb7d Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Wed, 9 Oct 2024 15:22:54 +0530 Subject: [PATCH 6/7] Seprated to NetworkIncrementByFloat --- libs/server/Resp/BasicCommands.cs | 78 ++++++++++++++++++--------- libs/server/Resp/RespServerSession.cs | 2 +- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 2f427368e4..9fd3f4395d 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -722,14 +722,13 @@ private bool NetworkIncrement(RespCommand cmd, ref TGarnetApi storag where TGarnetApi : IGarnetApi { Debug.Assert(cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY || cmd == RespCommand.INCR || - cmd == RespCommand.DECR || cmd == RespCommand.INCRBYFLOAT); + cmd == RespCommand.DECR); var key = parseState.GetArgSliceByRef(0); var sbKey = key.SpanByte; ArgSlice input = default; - var isFloat = cmd == RespCommand.INCRBYFLOAT; - if (cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY || isFloat) + if (cmd == RespCommand.INCRBY || cmd == RespCommand.DECRBY) { // Parse value argument // NOTE: Parse empty strings for better error messages through storageApi.Increment @@ -760,43 +759,70 @@ private bool NetworkIncrement(RespCommand cmd, ref TGarnetApi storag input = new ArgSlice(valPtr, vSize); } - Span outputBuffer = isFloat ? stackalloc byte[NumUtils.MaximumFormatDoubleLength + 1] : stackalloc byte[NumUtils.MaximumFormatInt64Length + 1]; + Span outputBuffer = stackalloc byte[NumUtils.MaximumFormatInt64Length + 1]; var output = ArgSlice.FromPinnedSpan(outputBuffer); storageApi.Increment(key, input, ref output); var errorFlag = OperationError.SUCCESS; - errorFlag = output.Length == (isFloat ? NumUtils.MaximumFormatDoubleLength : NumUtils.MaximumFormatInt64Length) + 1 + errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1 ? (OperationError)output.Span[0] : OperationError.SUCCESS; switch (errorFlag) { case OperationError.SUCCESS: - if (isFloat) - { - while (!RespWriteUtils.WriteBulkString(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) - SendAndReset(); - } - else - { - while (!RespWriteUtils.WriteIntegerFromBytes(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) - SendAndReset(); - } + while (!RespWriteUtils.WriteIntegerFromBytes(outputBuffer.Slice(0, output.Length), ref dcurr, dend)) + SendAndReset(); break; case OperationError.INVALID_TYPE: - if (isFloat) - { - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_NOT_VALID_FLOAT, ref dcurr, - dend)) - SendAndReset(); - } - else - { - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, + 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(); - } + SendAndReset(); break; default: throw new GarnetException($"Invalid OperationError {errorFlag}"); diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 52b55f2bf6..ac4df4174a 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -528,7 +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 => NetworkIncrement(RespCommand.INCRBYFLOAT, 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), From 2f48884fe2cdee1755f8547754b44cc2b54f43b1 Mon Sep 17 00:00:00 2001 From: Vijay-Nirmal Date: Thu, 10 Oct 2024 03:02:56 +0530 Subject: [PATCH 7/7] Added ClusterSlotVeficationTests --- .../RedirectTests/BaseCommand.cs | 17 +++++++++++++++++ .../ClusterSlotVerificationTests.cs | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs index 256235481e..a7ba54f99c 100644 --- a/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs +++ b/test/Garnet.test.cluster/RedirectTests/BaseCommand.cs @@ -259,6 +259,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 63649c2360..4b48a5dd9d 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(), @@ -147,6 +148,7 @@ public virtual void OneTimeTearDown() [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -225,6 +227,7 @@ void GarnetClientSessionClusterDown(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -315,6 +318,7 @@ void GarnetClientSessionOK(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -398,6 +402,7 @@ void GarnetClientSessionCrossslotTest(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -487,6 +492,7 @@ void GarnetClientSessionMOVEDTest(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")] @@ -593,6 +599,7 @@ void GarnetClientSessionASKTest(BaseCommand command) [TestCase("SETRANGE")] [TestCase("GETRANGE")] [TestCase("INCR")] + [TestCase("INCRBYFLOAT")] [TestCase("APPEND")] [TestCase("STRLEN")] [TestCase("RENAME")]