Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Compatibility] Added INCRBYFLOAT command #699

Merged
merged 10 commits into from
Oct 10, 2024
146 changes: 146 additions & 0 deletions libs/common/NumUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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,80 @@
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 into sequence of ASCII bytes
/// </summary>
/// <param name="value">Double value to convert</param>
/// <param name="length">Number of digits in value</param>

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Debug, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Debug, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Debug, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Debug, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Release, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Release, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Release, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (ubuntu-latest, net8.0, Release, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Debug, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Debug, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Debug, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Debug, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Release, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Release, Garnet.test)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Release, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name

Check warning on line 104 in libs/common/NumUtils.cs

View workflow job for this annotation

GitHub Actions / Garnet (windows-latest, net8.0, Release, Garnet.test.cluster)

XML comment has a param tag for 'length', but there is no parameter by that name
Vijay-Nirmal marked this conversation as resolved.
Show resolved Hide resolved
/// <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 +217,45 @@
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 +484,38 @@
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;
badrishc marked this conversation as resolved.
Show resolved Hide resolved

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
38 changes: 29 additions & 9 deletions libs/server/Resp/BasicCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -723,13 +723,14 @@ private bool NetworkIncrement<TGarnetApi>(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
Expand Down Expand Up @@ -760,24 +761,43 @@ private bool NetworkIncrement<TGarnetApi>(RespCommand cmd, ref TGarnetApi storag
input = new ArgSlice(valPtr, vSize);
}

Span<byte> outputBuffer = stackalloc byte[NumUtils.MaximumFormatInt64Length + 1];
Span<byte> 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}");
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 @@ -97,6 +97,7 @@ public enum RespCommand : byte
HSETNX,
INCR,
INCRBY,
INCRBYFLOAT,
badrishc marked this conversation as resolved.
Show resolved Hide resolved
LINSERT,
LMOVE,
LMPOP,
Expand Down Expand Up @@ -1346,6 +1347,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.SINTERSTORE;
}
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
29 changes: 29 additions & 0 deletions libs/server/Resp/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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 @@ -526,6 +526,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 => NetworkIncrement(RespCommand.INCRBYFLOAT, ref storageApi),
RespCommand.DECR => NetworkIncrement(RespCommand.DECR, ref storageApi),
RespCommand.DECRBY => NetworkIncrement(RespCommand.DECRBY, ref storageApi),
RespCommand.SETBIT => NetworkStringSetBit(ref storageApi),
Expand Down
Loading