Skip to content

Commit 01a58f6

Browse files
Vijay-Nirmalyrajas
andauthored
[Compatibility] Added LPOS command (#673)
* Added LPOS command * Fixed doc * Added RespCommand but it fails * Final commit before review comments changes :) * Fixed code style issues * Review comment fix * Fixed Buffer Copy length --------- Co-authored-by: Yoganand Rajasekaran <[email protected]>
1 parent bc8c7c0 commit 01a58f6

File tree

16 files changed

+544
-1
lines changed

16 files changed

+544
-1
lines changed

libs/common/RespWriteUtils.cs

+12
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ public static bool WriteArrayLength(int len, ref byte* curr, byte* end)
7878
return true;
7979
}
8080

81+
public static bool WriteArrayLength(int len, ref byte* curr, byte* end, out int numDigits, out int totalLen)
82+
{
83+
numDigits = NumUtils.NumDigits(len);
84+
totalLen = 1 + numDigits + 2;
85+
if (totalLen > (int)(end - curr))
86+
return false;
87+
*curr++ = (byte)'*';
88+
NumUtils.IntToBytes(len, numDigits, ref curr);
89+
WriteNewline(ref curr);
90+
return true;
91+
}
92+
8193
/// <summary>
8294
/// Write array item
8395
/// </summary>

libs/server/API/GarnetApiObjectCommands.cs

+4
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ public GarnetStatus ListLeftPush(ArgSlice key, ArgSlice element, out int count,
175175
public GarnetStatus ListLeftPush(byte[] key, ref ObjectInput input, out ObjectOutputHeader output)
176176
=> storageSession.ListPush(key, ref input, out output, ref objectContext);
177177

178+
/// <inheritdoc />
179+
public GarnetStatus ListPosition(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter)
180+
=> storageSession.ListPosition(key, ref input, ref outputFooter, ref objectContext);
181+
178182
/// <inheritdoc />
179183
public GarnetStatus ListLeftPop(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter)
180184
=> storageSession.ListPop(key, ref input, ref outputFooter, ref objectContext);

libs/server/API/IGarnetApi.cs

+10
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,16 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
599599

600600
#region ListPush Methods
601601

602+
/// <summary>
603+
/// The command returns the index of matching elements inside a Redis list.
604+
/// By default, when no options are given, it will scan the list from head to tail, looking for the first match of "element".
605+
/// </summary>
606+
/// <param name="key"></param>
607+
/// <param name="input"></param>
608+
/// <param name="outputFooter"></param>
609+
/// <returns></returns>
610+
GarnetStatus ListPosition(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter);
611+
602612
/// <summary>
603613
/// ListLeftPush ArgSlice version with ObjectOutputHeader output
604614
/// </summary>

libs/server/Objects/List/ListObject.cs

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public enum ListOperation : byte
3434
LSET,
3535
BRPOP,
3636
BLPOP,
37+
LPOS,
3738
}
3839

3940
/// <summary>
@@ -179,6 +180,9 @@ public override unsafe bool Operate(ref ObjectInput input, ref SpanByteAndMemory
179180
case ListOperation.LSET:
180181
ListSet(ref input, ref output);
181182
break;
183+
case ListOperation.LPOS:
184+
ListPosition(ref input, ref output);
185+
break;
182186

183187
default:
184188
throw new GarnetException($"Unsupported operation {input.header.ListOp} in ListObject.Operate");

libs/server/Objects/List/ListObjectImpl.cs

+212
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Buffers;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Linq;
89
using Garnet.common;
910
using Tsavorite.core;
@@ -418,5 +419,216 @@ private void ListSet(ref ObjectInput input, ref SpanByteAndMemory output)
418419
output.Length = (int)(output_currptr - output_startptr);
419420
}
420421
}
422+
423+
private void ListPosition(ref ObjectInput input, ref SpanByteAndMemory output)
424+
{
425+
var element = input.parseState.GetArgSliceByRef(input.parseStateStartIdx).ReadOnlySpan;
426+
input.parseStateStartIdx++;
427+
428+
var isMemory = false;
429+
MemoryHandle ptrHandle = default;
430+
var output_startptr = output.SpanByte.ToPointer();
431+
var output_currptr = output_startptr;
432+
var output_end = output_currptr + output.Length;
433+
var count = 0;
434+
var isDefaultCount = true;
435+
ObjectOutputHeader outputHeader = default;
436+
437+
try
438+
{
439+
if (!ReadListPositionInput(ref input, out var rank, out count, out isDefaultCount, out var maxlen, out var error))
440+
{
441+
while (!RespWriteUtils.WriteError(error, ref output_currptr, output_end))
442+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
443+
return;
444+
}
445+
446+
if (count < 0)
447+
{
448+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref output_currptr, output_end))
449+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
450+
return;
451+
}
452+
453+
if (maxlen < 0)
454+
{
455+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref output_currptr, output_end))
456+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
457+
return;
458+
}
459+
460+
if (rank == 0)
461+
{
462+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER, ref output_currptr, output_end))
463+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
464+
return;
465+
}
466+
467+
count = count == 0 ? list.Count : count;
468+
var totalArrayHeaderLen = 0;
469+
var lastFoundItemIndex = -1;
470+
471+
if (!isDefaultCount)
472+
{
473+
while (!RespWriteUtils.WriteArrayLength(count, ref output_currptr, output_end, out var _, out totalArrayHeaderLen))
474+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
475+
}
476+
477+
var noOfFoundItem = 0;
478+
if (rank > 0)
479+
{
480+
var currentNode = list.First;
481+
var currentIndex = 0;
482+
var maxlenIndex = maxlen == 0 ? list.Count : maxlen;
483+
do
484+
{
485+
var nextNode = currentNode.Next;
486+
if (currentNode.Value.AsSpan().SequenceEqual(element))
487+
{
488+
if (rank == 1)
489+
{
490+
lastFoundItemIndex = currentIndex;
491+
while (!RespWriteUtils.WriteInteger(currentIndex, ref output_currptr, output_end))
492+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
493+
494+
noOfFoundItem++;
495+
if (noOfFoundItem == count)
496+
{
497+
break;
498+
}
499+
}
500+
else
501+
{
502+
rank--;
503+
}
504+
}
505+
currentNode = nextNode;
506+
currentIndex++;
507+
}
508+
while (currentNode != null && currentIndex < maxlenIndex);
509+
}
510+
else // (rank < 0)
511+
{
512+
var currentNode = list.Last;
513+
var currentIndex = list.Count - 1;
514+
var maxlenIndex = maxlen == 0 ? 0 : list.Count - maxlen;
515+
do
516+
{
517+
var nextNode = currentNode.Previous;
518+
if (currentNode.Value.AsSpan().SequenceEqual(element))
519+
{
520+
if (rank == -1)
521+
{
522+
lastFoundItemIndex = currentIndex;
523+
while (!RespWriteUtils.WriteInteger(currentIndex, ref output_currptr, output_end))
524+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
525+
526+
noOfFoundItem++;
527+
if (noOfFoundItem == count)
528+
{
529+
break;
530+
}
531+
}
532+
else
533+
{
534+
rank++;
535+
}
536+
}
537+
currentNode = nextNode;
538+
currentIndex--;
539+
}
540+
while (currentNode != null && currentIndex >= maxlenIndex);
541+
}
542+
543+
if (isDefaultCount && noOfFoundItem == 0)
544+
{
545+
output_currptr = output_startptr;
546+
while (!RespWriteUtils.WriteNull(ref output_currptr, output_end))
547+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
548+
}
549+
else if (!isDefaultCount && noOfFoundItem == 0)
550+
{
551+
output_currptr = output_startptr;
552+
while (!RespWriteUtils.WriteNullArray(ref output_currptr, output_end))
553+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
554+
}
555+
else if (!isDefaultCount && noOfFoundItem != count)
556+
{
557+
var newTotalArrayHeaderLen = 0;
558+
var startOutputStartptr = output_startptr;
559+
RespWriteUtils.WriteArrayLength(noOfFoundItem, ref startOutputStartptr, output_end, out var _, out newTotalArrayHeaderLen); // ReallocateOutput is not needed here as there should be always be available space in the output buffer as we have already written the max array length
560+
Debug.Assert(totalArrayHeaderLen >= newTotalArrayHeaderLen, "newTotalArrayHeaderLen can't be bigger than totalArrayHeaderLen as we have already written max array lenght in the buffer");
561+
562+
if (totalArrayHeaderLen != newTotalArrayHeaderLen)
563+
{
564+
var remainingLength = (output_currptr - output_startptr) - totalArrayHeaderLen;
565+
Buffer.MemoryCopy(output_startptr + totalArrayHeaderLen, output_startptr + newTotalArrayHeaderLen, remainingLength, remainingLength);
566+
output_currptr = output_currptr - (totalArrayHeaderLen - newTotalArrayHeaderLen);
567+
}
568+
}
569+
570+
outputHeader.result1 = noOfFoundItem;
571+
}
572+
finally
573+
{
574+
while (!RespWriteUtils.WriteDirect(ref outputHeader, ref output_currptr, output_end))
575+
ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref output_startptr, ref ptrHandle, ref output_currptr, ref output_end);
576+
577+
if (isMemory)
578+
ptrHandle.Dispose();
579+
output.Length = (int)(output_currptr - output_startptr);
580+
}
581+
}
582+
583+
private static unsafe bool ReadListPositionInput(ref ObjectInput input, out int rank, out int count, out bool isDefaultCount, out int maxlen, out ReadOnlySpan<byte> error)
584+
{
585+
var currTokenIdx = input.parseStateStartIdx;
586+
587+
rank = 1; // By default, LPOS takes first match element
588+
count = 1; // By default, LPOS return 1 element
589+
isDefaultCount = true;
590+
maxlen = 0; // By default, iterate to all the item
591+
592+
error = default;
593+
594+
while (currTokenIdx < input.parseState.Count)
595+
{
596+
var sbParam = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan;
597+
598+
if (sbParam.SequenceEqual(CmdStrings.RANK) || sbParam.SequenceEqual(CmdStrings.rank))
599+
{
600+
if (!input.parseState.TryGetInt(currTokenIdx++, out rank))
601+
{
602+
error = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
603+
return false;
604+
}
605+
}
606+
else if (sbParam.SequenceEqual(CmdStrings.COUNT) || sbParam.SequenceEqual(CmdStrings.count))
607+
{
608+
if (!input.parseState.TryGetInt(currTokenIdx++, out count))
609+
{
610+
error = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
611+
return false;
612+
}
613+
614+
isDefaultCount = false;
615+
}
616+
else if (sbParam.SequenceEqual(CmdStrings.MAXLEN) || sbParam.SequenceEqual(CmdStrings.maxlen))
617+
{
618+
if (!input.parseState.TryGetInt(currTokenIdx++, out maxlen))
619+
{
620+
error = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
621+
return false;
622+
}
623+
}
624+
else
625+
{
626+
error = CmdStrings.RESP_SYNTAX_ERROR;
627+
return false;
628+
}
629+
}
630+
631+
return true;
632+
}
421633
}
422634
}

libs/server/Resp/CmdStrings.cs

+4
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ static partial class CmdStrings
9595
public static ReadOnlySpan<byte> XX => "XX"u8;
9696
public static ReadOnlySpan<byte> UNSAFETRUNCATELOG => "UNSAFETRUNCATELOG"u8;
9797
public static ReadOnlySpan<byte> SAMPLES => "SAMPLES"u8;
98+
public static ReadOnlySpan<byte> RANK => "RANK"u8;
99+
public static ReadOnlySpan<byte> rank => "rank"u8;
100+
public static ReadOnlySpan<byte> MAXLEN => "MAXLEN"u8;
101+
public static ReadOnlySpan<byte> maxlen => "maxlen"u8;
98102

99103
/// <summary>
100104
/// Response strings

libs/server/Resp/Objects/ListCommands.cs

+59
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,65 @@ private unsafe bool ListPop<TGarnetApi>(RespCommand command, ref TGarnetApi stor
156156
return true;
157157
}
158158

159+
/// <summary>
160+
/// The command returns the index of matching elements inside a Redis list.
161+
/// By default, when no options are given, it will scan the list from head to tail, looking for the first match of "element".
162+
/// </summary>
163+
/// <typeparam name="TGarnetApi"></typeparam>
164+
/// <param name="storageApi"></param>
165+
/// <returns></returns>
166+
private unsafe bool ListPosition<TGarnetApi>(ref TGarnetApi storageApi)
167+
where TGarnetApi : IGarnetApi
168+
{
169+
if (parseState.Count < 2)
170+
{
171+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.LPOS));
172+
}
173+
174+
// Get the key for List
175+
var sbKey = parseState.GetArgSliceByRef(0).SpanByte;
176+
var element = parseState.GetArgSliceByRef(1).SpanByte;
177+
var keyBytes = sbKey.ToByteArray();
178+
179+
if (NetworkSingleKeySlotVerify(keyBytes, false))
180+
{
181+
return true;
182+
}
183+
184+
// Prepare input
185+
var input = new ObjectInput
186+
{
187+
header = new RespInputHeader
188+
{
189+
type = GarnetObjectType.List,
190+
ListOp = ListOperation.LPOS,
191+
},
192+
parseState = parseState,
193+
parseStateStartIdx = 1,
194+
};
195+
196+
// Prepare GarnetObjectStore output
197+
var outputFooter = new GarnetObjectStoreOutput { spanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) };
198+
199+
var statusOp = storageApi.ListPosition(keyBytes, ref input, ref outputFooter);
200+
201+
switch (statusOp)
202+
{
203+
case GarnetStatus.OK:
204+
ProcessOutputWithHeader(outputFooter.spanByteAndMemory);
205+
break;
206+
case GarnetStatus.NOTFOUND:
207+
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend))
208+
SendAndReset();
209+
break;
210+
case GarnetStatus.WRONGTYPE:
211+
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend))
212+
SendAndReset();
213+
break;
214+
}
215+
216+
return true;
217+
}
159218

160219
/// <summary>
161220
/// LMPOP numkeys key [key ...] LEFT | RIGHT [COUNT count]

libs/server/Resp/Parser/RespCommand.cs

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public enum RespCommand : byte
4545
KEYS,
4646
LINDEX,
4747
LLEN,
48+
LPOS,
4849
LRANGE,
4950
MEMORY_USAGE,
5051
MGET,
@@ -770,6 +771,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
770771
{
771772
return RespCommand.LSET;
772773
}
774+
else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read<ulong>("\r\nLPOS\r\n"u8))
775+
{
776+
return RespCommand.LPOS;
777+
}
773778
break;
774779

775780
case 'M':

0 commit comments

Comments
 (0)