Skip to content

Commit ace7cb6

Browse files
[Compatibility] Added INCRBYFLOAT command (#699)
* 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 <[email protected]>
1 parent 0da61f5 commit ace7cb6

File tree

16 files changed

+532
-6
lines changed

16 files changed

+532
-6
lines changed

libs/common/NumUtils.cs

+147
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace Garnet.common
1313
public static unsafe class NumUtils
1414
{
1515
public const int MaximumFormatInt64Length = 20; // 19 + sign (i.e. -9223372036854775808)
16+
public const int MaximumFormatDoubleLength = 310; // (i.e. -1.7976931348623157E+308)
1617

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

79+
/// <summary>
80+
/// Convert double number into sequence of ASCII bytes
81+
/// </summary>
82+
/// <param name="value">Value to convert</param>
83+
/// <param name="dest">Span Byte</param>
84+
/// <returns>Length of number in result</returns>
85+
public static int DoubleToSpanByte(double value, Span<byte> dest)
86+
{
87+
int totalLen = NumOfCharInDouble(value, out var integerDigits, out var signSize, out var fractionalDigits);
88+
bool isNegative = value < 0;
89+
if (totalLen > dest.Length)
90+
return 0;
91+
fixed (byte* ptr = dest)
92+
{
93+
byte* curr = ptr;
94+
DoubleToBytes(value, integerDigits, fractionalDigits, ref curr);
95+
}
96+
97+
return totalLen;
98+
}
99+
100+
/// <summary>
101+
/// Convert double number into sequence of ASCII bytes
102+
/// </summary>
103+
/// <param name="value">Value to convert</param>
104+
/// <param name="integerDigits">Number of digits in the integer part of the double value</param>
105+
/// <param name="fractionalDigits">Number of digits in the fractional part of the double value</param>
106+
/// <param name="result">Byte pointer, will be updated to point after the written number</param>
107+
public static unsafe void DoubleToBytes(double value, int integerDigits, int fractionalDigits, ref byte* result)
108+
{
109+
Debug.Assert(!double.IsNaN(value) && !double.IsInfinity(value), "Cannot convert NaN or Infinity to bytes.");
110+
111+
if (value == 0)
112+
{
113+
*result++ = (byte)'0';
114+
return;
115+
}
116+
117+
bool isNegative = value < 0;
118+
if (isNegative)
119+
{
120+
*result++ = (byte)'-';
121+
value = -value;
122+
}
123+
124+
result += integerDigits;
125+
var integerPart = Math.Truncate(value);
126+
double fractionalPart = fractionalDigits > 0 ? Math.Round(value - integerPart, fractionalDigits) : 0;
127+
128+
// Convert integer part
129+
do
130+
{
131+
*--result = (byte)((byte)'0' + (integerPart % 10));
132+
integerPart /= 10;
133+
} while (integerPart >= 1);
134+
result += integerDigits;
135+
136+
if (fractionalDigits > 0)
137+
{
138+
// Add decimal point
139+
*result++ = (byte)'.';
140+
141+
// Convert fractional part
142+
for (int i = 0; i < fractionalDigits; i++)
143+
{
144+
fractionalPart *= 10;
145+
int digit = (int)fractionalPart;
146+
*result++ = (byte)((byte)'0' + digit);
147+
fractionalPart = Math.Round(fractionalPart - digit, fractionalDigits - i - 1);
148+
}
149+
150+
result--; // Move back to the last digit
151+
}
152+
}
153+
78154
/// <summary>
79155
/// Convert sequence of ASCII bytes into long number
80156
/// </summary>
@@ -142,6 +218,45 @@ public static bool TryBytesToLong(int length, byte* source, out long result)
142218
return true;
143219
}
144220

221+
/// <summary>
222+
/// Convert sequence of ASCII bytes into double number
223+
/// </summary>
224+
/// <param name="source">Source bytes</param>
225+
/// <param name="result">Double value extracted from sequence</param>
226+
/// <returns>True if sequence contains only numeric digits, otherwise false</returns>
227+
public static bool TryBytesToDouble(ReadOnlySpan<byte> source, out double result)
228+
{
229+
fixed (byte* ptr = source)
230+
return TryBytesToDouble(source.Length, ptr, out result);
231+
}
232+
233+
/// <summary>
234+
/// Convert sequence of ASCII bytes into double number
235+
/// </summary>
236+
/// <param name="length">Length of number</param>
237+
/// <param name="source">Source bytes</param>
238+
/// <param name="result">Double value extracted from sequence</param>
239+
/// <returns>True if sequence contains only numeric digits, otherwise false</returns>
240+
public static bool TryBytesToDouble(int length, byte* source, out double result)
241+
{
242+
var fNeg = *source == '-';
243+
var beg = fNeg ? source + 1 : source;
244+
var len = fNeg ? length - 1 : length;
245+
result = 0;
246+
247+
// Do not allow leading zeros
248+
if (len > 1 && *beg == '0' && *(beg + 1) != '.')
249+
return false;
250+
251+
// Parse number and check consumed bytes to avoid alphanumeric strings
252+
if (!TryParse(new ReadOnlySpan<byte>(beg, len), out result))
253+
return false;
254+
255+
// Negate if parsed value has a leading negative sign
256+
result = fNeg ? -result : result;
257+
return true;
258+
}
259+
145260
/// <summary>
146261
/// Convert sequence of ASCII bytes into ulong number
147262
/// </summary>
@@ -370,6 +485,38 @@ public static int NumDigitsInLong(long v, ref bool fNeg)
370485
return 19;
371486
}
372487

488+
/// <summary>
489+
/// Return number of digits in given double number incluing the decimal part and `.` character
490+
/// </summary>
491+
/// <param name="v">Double value</param>
492+
/// <returns>Number of digits in the integer part of the double value</returns>
493+
public static int NumOfCharInDouble(double v, out int integerDigits, out byte signSize, out int fractionalDigits)
494+
{
495+
if (v == 0)
496+
{
497+
integerDigits = 1;
498+
signSize = 0;
499+
fractionalDigits = 0;
500+
return 1;
501+
}
502+
503+
Debug.Assert(!double.IsNaN(v) && !double.IsInfinity(v));
504+
505+
signSize = (byte)(v < 0 ? 1 : 0); // Add sign if the number is negative
506+
v = Math.Abs(v);
507+
integerDigits = (int)Math.Log10(v) + 1;
508+
509+
fractionalDigits = 0; // Max of 15 significant digits
510+
while (fractionalDigits <= 14 && Math.Abs(v - Math.Round(v, fractionalDigits)) > 2 * Double.Epsilon) // 2 * Double.Epsilon is used to handle floating point errors
511+
{
512+
fractionalDigits++;
513+
}
514+
515+
var dotSize = fractionalDigits != 0 ? 1 : 0; // Add decimal point if there are significant digits
516+
517+
return signSize + integerDigits + dotSize + fractionalDigits;
518+
}
519+
373520
/// <inheritdoc cref="Utf8Parser.TryParse(ReadOnlySpan{byte}, out int, out int, char)"/>
374521
public static bool TryParse(ReadOnlySpan<byte> source, out int value)
375522
{

libs/resources/RespCommandsInfo.json

+29
Original file line numberDiff line numberDiff line change
@@ -1848,6 +1848,35 @@
18481848
}
18491849
]
18501850
},
1851+
{
1852+
"Command": "INCRBYFLOAT",
1853+
"Name": "INCRBYFLOAT",
1854+
"IsInternal": false,
1855+
"Arity": 3,
1856+
"Flags": "DenyOom, Fast, Write",
1857+
"FirstKey": 1,
1858+
"LastKey": 1,
1859+
"Step": 1,
1860+
"AclCategories": "Fast, String, Write",
1861+
"Tips": null,
1862+
"KeySpecifications": [
1863+
{
1864+
"BeginSearch": {
1865+
"TypeDiscriminator": "BeginSearchIndex",
1866+
"Index": 1
1867+
},
1868+
"FindKeys": {
1869+
"TypeDiscriminator": "FindKeysRange",
1870+
"LastKey": 0,
1871+
"KeyStep": 1,
1872+
"Limit": 0
1873+
},
1874+
"Notes": null,
1875+
"Flags": "RW, Access, Update"
1876+
}
1877+
],
1878+
"SubCommands": null
1879+
},
18511880
{
18521881
"Command": "INFO",
18531882
"Name": "INFO",

libs/server/Resp/BasicCommands.cs

+49-3
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,9 @@ private bool NetworkIncrement<TGarnetApi>(RespCommand cmd, ref TGarnetApi storag
758758
var output = ArgSlice.FromPinnedSpan(outputBuffer);
759759

760760
storageApi.Increment(key, input, ref output);
761-
var errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1
761+
762+
var errorFlag = OperationError.SUCCESS;
763+
errorFlag = output.Length == NumUtils.MaximumFormatInt64Length + 1
762764
? (OperationError)output.Span[0]
763765
: OperationError.SUCCESS;
764766

@@ -769,8 +771,52 @@ private bool NetworkIncrement<TGarnetApi>(RespCommand cmd, ref TGarnetApi storag
769771
SendAndReset();
770772
break;
771773
case OperationError.INVALID_TYPE:
772-
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr,
773-
dend))
774+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref dcurr, dend))
775+
SendAndReset();
776+
break;
777+
default:
778+
throw new GarnetException($"Invalid OperationError {errorFlag}");
779+
}
780+
781+
return true;
782+
}
783+
784+
/// <summary>
785+
/// Increment by float (INCRBYFLOAT)
786+
/// </summary>
787+
private bool NetworkIncrementByFloat<TGarnetApi>(ref TGarnetApi storageApi)
788+
where TGarnetApi : IGarnetApi
789+
{
790+
var key = parseState.GetArgSliceByRef(0);
791+
var sbKey = key.SpanByte;
792+
793+
ArgSlice input = default;
794+
var sbVal = parseState.GetArgSliceByRef(1).SpanByte;
795+
var valPtr = sbVal.ToPointer() - RespInputHeader.Size;
796+
var vSize = sbVal.Length + RespInputHeader.Size;
797+
((RespInputHeader*)valPtr)->cmd = RespCommand.INCRBYFLOAT;
798+
((RespInputHeader*)valPtr)->flags = 0;
799+
input = new ArgSlice(valPtr, vSize);
800+
801+
Span<byte> outputBuffer = stackalloc byte[NumUtils.MaximumFormatDoubleLength + 1];
802+
var output = ArgSlice.FromPinnedSpan(outputBuffer);
803+
804+
storageApi.Increment(key, input, ref output);
805+
806+
var errorFlag = OperationError.SUCCESS;
807+
errorFlag = output.Length == NumUtils.MaximumFormatDoubleLength + 1
808+
? (OperationError)output.Span[0]
809+
: OperationError.SUCCESS;
810+
811+
switch (errorFlag)
812+
{
813+
case OperationError.SUCCESS:
814+
while (!RespWriteUtils.WriteBulkString(outputBuffer.Slice(0, output.Length), ref dcurr, dend))
815+
SendAndReset();
816+
break;
817+
case OperationError.INVALID_TYPE:
818+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_NOT_VALID_FLOAT, ref dcurr,
819+
dend))
774820
SendAndReset();
775821
break;
776822
default:

libs/server/Resp/Parser/RespCommand.cs

+5
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public enum RespCommand : byte
100100
HSETNX,
101101
INCR,
102102
INCRBY,
103+
INCRBYFLOAT,
103104
LINSERT,
104105
LMOVE,
105106
LMPOP,
@@ -1368,6 +1369,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
13681369
{
13691370
return RespCommand.PEXPIRETIME;
13701371
}
1372+
else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read<ulong>("1\r\nINCRB"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read<ulong>("YFLOAT\r\n"u8))
1373+
{
1374+
return RespCommand.INCRBYFLOAT;
1375+
}
13711376
break;
13721377

13731378
case 12:

libs/server/Resp/RespServerSession.cs

+1
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ private bool ProcessBasicCommands<TGarnetApi>(RespCommand cmd, ref TGarnetApi st
528528
RespCommand.STRLEN => NetworkSTRLEN(ref storageApi),
529529
RespCommand.INCR => NetworkIncrement(RespCommand.INCR, ref storageApi),
530530
RespCommand.INCRBY => NetworkIncrement(RespCommand.INCRBY, ref storageApi),
531+
RespCommand.INCRBYFLOAT => NetworkIncrementByFloat(ref storageApi),
531532
RespCommand.DECR => NetworkIncrement(RespCommand.DECR, ref storageApi),
532533
RespCommand.DECRBY => NetworkIncrement(RespCommand.DECRBY, ref storageApi),
533534
RespCommand.SETBIT => NetworkStringSetBit(ref storageApi),

0 commit comments

Comments
 (0)