Skip to content

Commit

Permalink
[Compatibility] Added INCRBYFLOAT command (#699)
Browse files Browse the repository at this point in the history
* Added INCRBYFLOAT command

* Fixed code format issue

* Added ACL test

* Fixed the warning

* Removed unused import

* Seprated to NetworkIncrementByFloat

* Added ClusterSlotVeficationTests

---------

Co-authored-by: Badrish Chandramouli <badrishc@microsoft.com>
  • Loading branch information
Vijay-Nirmal and badrishc authored Oct 10, 2024
1 parent 0da61f5 commit ace7cb6
Show file tree
Hide file tree
Showing 16 changed files with 532 additions and 6 deletions.
147 changes: 147 additions & 0 deletions libs/common/NumUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

/// <summary>
/// Convert long number into sequence of ASCII bytes
Expand Down Expand Up @@ -75,6 +76,81 @@ public static unsafe void LongToBytes(long value, int length, ref byte* result)
result += length;
}

/// <summary>
/// Convert double number into sequence of ASCII bytes
/// </summary>
/// <param name="value">Value to convert</param>
/// <param name="dest">Span Byte</param>
/// <returns>Length of number in result</returns>
public static int DoubleToSpanByte(double value, Span<byte> 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;
}

/// <summary>
/// Convert double number into sequence of ASCII bytes
/// </summary>
/// <param name="value">Value to convert</param>
/// <param name="integerDigits">Number of digits in the integer part of the double value</param>
/// <param name="fractionalDigits">Number of digits in the fractional part of the double value</param>
/// <param name="result">Byte pointer, will be updated to point after the written number</param>
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
}
}

/// <summary>
/// Convert sequence of ASCII bytes into long number
/// </summary>
Expand Down Expand Up @@ -142,6 +218,45 @@ public static bool TryBytesToLong(int length, byte* source, out long result)
return true;
}

/// <summary>
/// Convert sequence of ASCII bytes into double number
/// </summary>
/// <param name="source">Source bytes</param>
/// <param name="result">Double value extracted from sequence</param>
/// <returns>True if sequence contains only numeric digits, otherwise false</returns>
public static bool TryBytesToDouble(ReadOnlySpan<byte> source, out double result)
{
fixed (byte* ptr = source)
return TryBytesToDouble(source.Length, ptr, out result);
}

/// <summary>
/// Convert sequence of ASCII bytes into double number
/// </summary>
/// <param name="length">Length of number</param>
/// <param name="source">Source bytes</param>
/// <param name="result">Double value extracted from sequence</param>
/// <returns>True if sequence contains only numeric digits, otherwise false</returns>
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<byte>(beg, len), out result))
return false;

// Negate if parsed value has a leading negative sign
result = fNeg ? -result : result;
return true;
}

/// <summary>
/// Convert sequence of ASCII bytes into ulong number
/// </summary>
Expand Down Expand Up @@ -370,6 +485,38 @@ public static int NumDigitsInLong(long v, ref bool fNeg)
return 19;
}

/// <summary>
/// Return number of digits in given double number incluing the decimal part and `.` character
/// </summary>
/// <param name="v">Double value</param>
/// <returns>Number of digits in the integer part of the double value</returns>
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;
}

/// <inheritdoc cref="Utf8Parser.TryParse(ReadOnlySpan{byte}, out int, out int, char)"/>
public static bool TryParse(ReadOnlySpan<byte> source, out int value)
{
Expand Down
29 changes: 29 additions & 0 deletions libs/resources/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -1848,6 +1848,35 @@
}
]
},
{
"Command": "INCRBYFLOAT",
"Name": "INCRBYFLOAT",
"IsInternal": false,
"Arity": 3,
"Flags": "DenyOom, Fast, Write",
"FirstKey": 1,
"LastKey": 1,
"Step": 1,
"AclCategories": "Fast, String, Write",
"Tips": null,
"KeySpecifications": [
{
"BeginSearch": {
"TypeDiscriminator": "BeginSearchIndex",
"Index": 1
},
"FindKeys": {
"TypeDiscriminator": "FindKeysRange",
"LastKey": 0,
"KeyStep": 1,
"Limit": 0
},
"Notes": null,
"Flags": "RW, Access, Update"
}
],
"SubCommands": null
},
{
"Command": "INFO",
"Name": "INFO",
Expand Down
52 changes: 49 additions & 3 deletions libs/server/Resp/BasicCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,9 @@ private bool NetworkIncrement<TGarnetApi>(RespCommand cmd, ref TGarnetApi storag
var output = ArgSlice.FromPinnedSpan(outputBuffer);

storageApi.Increment(key, input, ref output);
var errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1

var errorFlag = OperationError.SUCCESS;
errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1
? (OperationError)output.Span[0]
: OperationError.SUCCESS;

Expand All @@ -769,8 +771,52 @@ private bool NetworkIncrement<TGarnetApi>(RespCommand cmd, ref TGarnetApi storag
SendAndReset();
break;
case OperationError.INVALID_TYPE:
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr,
dend))
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend))
SendAndReset();
break;
default:
throw new GarnetException($"Invalid OperationError {errorFlag}");
}

return true;
}

/// <summary>
/// Increment by float (INCRBYFLOAT)
/// </summary>
private bool NetworkIncrementByFloat<TGarnetApi>(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<byte> outputBuffer = stackalloc byte[NumUtils.MaximumFormatDoubleLength + 1];
var output = ArgSlice.FromPinnedSpan(outputBuffer);

storageApi.Increment(key, input, ref output);

var errorFlag = OperationError.SUCCESS;
errorFlag = output.Length == NumUtils.MaximumFormatDoubleLength + 1
? (OperationError)output.Span[0]
: OperationError.SUCCESS;

switch (errorFlag)
{
case OperationError.SUCCESS:
while (!RespWriteUtils.WriteBulkString(outputBuffer.Slice(0, output.Length), ref dcurr, dend))
SendAndReset();
break;
case OperationError.INVALID_TYPE:
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_NOT_VALID_FLOAT, ref dcurr,
dend))
SendAndReset();
break;
default:
Expand Down
5 changes: 5 additions & 0 deletions libs/server/Resp/Parser/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public enum RespCommand : byte
HSETNX,
INCR,
INCRBY,
INCRBYFLOAT,
LINSERT,
LMOVE,
LMPOP,
Expand Down Expand Up @@ -1368,6 +1369,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.PEXPIRETIME;
}
else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read<ulong>("1\r\nINCRB"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read<ulong>("YFLOAT\r\n"u8))
{
return RespCommand.INCRBYFLOAT;
}
break;

case 12:
Expand Down
1 change: 1 addition & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ private bool ProcessBasicCommands<TGarnetApi>(RespCommand cmd, ref TGarnetApi st
RespCommand.STRLEN => NetworkSTRLEN(ref storageApi),
RespCommand.INCR => NetworkIncrement(RespCommand.INCR, ref storageApi),
RespCommand.INCRBY => NetworkIncrement(RespCommand.INCRBY, ref storageApi),
RespCommand.INCRBYFLOAT => NetworkIncrementByFloat(ref storageApi),
RespCommand.DECR => NetworkIncrement(RespCommand.DECR, ref storageApi),
RespCommand.DECRBY => NetworkIncrement(RespCommand.DECRBY, ref storageApi),
RespCommand.SETBIT => NetworkStringSetBit(ref storageApi),
Expand Down
Loading

0 comments on commit ace7cb6

Please sign in to comment.