Skip to content

Commit

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

* Format fix

* Removed error case

* Review command fixes

* Revert CommandDocsUpdater changes

* Review command fix

* Fixed test failure
  • Loading branch information
Vijay-Nirmal authored Dec 7, 2024
1 parent ebb9071 commit c07afca
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 2 deletions.
31 changes: 31 additions & 0 deletions libs/resources/RespCommandsDocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4987,6 +4987,37 @@
}
]
},
{
"Command": "SINTERCARD",
"Name": "SINTERCARD",
"Summary": "Returns the number of members of the intersect of multiple sets.",
"Group": "Set",
"Complexity": "O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.",
"Arguments": [
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "NUMKEYS",
"DisplayText": "numkeys",
"Type": "Integer"
},
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"ArgumentFlags": "Multiple",
"KeySpecIndex": 0
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "LIMIT",
"DisplayText": "limit",
"Type": "Integer",
"Token": "LIMIT",
"ArgumentFlags": "Optional"
}
]
},
{
"Command": "SINTERSTORE",
"Name": "SINTERSTORE",
Expand Down
22 changes: 22 additions & 0 deletions libs/resources/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -3515,6 +3515,28 @@
}
]
},
{
"Command": "SINTERCARD",
"Name": "SINTERCARD",
"Arity": -3,
"Flags": "MovableKeys, ReadOnly",
"AclCategories": "Read, Set, Slow",
"KeySpecifications": [
{
"BeginSearch": {
"TypeDiscriminator": "BeginSearchIndex",
"Index": 1
},
"FindKeys": {
"TypeDiscriminator": "FindKeysKeyNum",
"KeyNumIdx": 0,
"FirstKey": 1,
"KeyStep": 1
},
"Flags": "RO, Access"
}
]
},
{
"Command": "SINTERSTORE",
"Name": "SINTERSTORE",
Expand Down
4 changes: 4 additions & 0 deletions libs/server/API/GarnetApiObjectCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ public GarnetStatus SetDiffStore(byte[] key, ArgSlice[] keys, out int count)
public GarnetStatus SetIntersect(ArgSlice[] keys, out HashSet<byte[]> output)
=> storageSession.SetIntersect(keys, out output);

/// <inheritdoc />
public GarnetStatus SetIntersectLength(ReadOnlySpan<ArgSlice> keys, int? limit, out int count)
=> storageSession.SetIntersectLength(keys, limit, out count);

/// <inheritdoc />
public GarnetStatus SetIntersectStore(byte[] key, ArgSlice[] keys, out int count)
=> storageSession.SetIntersectStore(key, keys, out count);
Expand Down
9 changes: 9 additions & 0 deletions libs/server/API/GarnetWatchApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,15 @@ public GarnetStatus SetDiff(ArgSlice[] keys, out HashSet<byte[]> output)
}
return garnetApi.SetDiff(keys, out output);
}

public GarnetStatus SetIntersectLength(ReadOnlySpan<ArgSlice> keys, int? limit, out int count)
{
foreach (var key in keys)
{
garnetApi.WATCH(key, StoreType.Object);
}
return garnetApi.SetIntersectLength(keys, limit, out count);
}
#endregion

#region Hash Methods
Expand Down
10 changes: 10 additions & 0 deletions libs/server/API/IGarnetApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,16 @@ public interface IGarnetReadApi
/// <param name="members"></param>
/// <returns></returns>
GarnetStatus SetDiff(ArgSlice[] keys, out HashSet<byte[]> members);

/// <summary>
/// Returns the cardinality of the intersection between multiple sets.
/// When limit is greater than 0, stops counting when reaching limit.
/// </summary>
/// <param name="keys">Keys of the sets to intersect</param>
/// <param name="limit">Optional limit to stop counting at</param>
/// <param name="count">The cardinality of the intersection</param>
/// <returns>Operation status</returns>
GarnetStatus SetIntersectLength(ReadOnlySpan<ArgSlice> keys, int? limit, out int count);
#endregion

#region Hash Methods
Expand Down
3 changes: 3 additions & 0 deletions libs/server/Resp/CmdStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> LEFT => "LEFT"u8;
public static ReadOnlySpan<byte> BYLEX => "BYLEX"u8;
public static ReadOnlySpan<byte> REV => "REV"u8;
public static ReadOnlySpan<byte> LIMIT => "LIMIT"u8;

/// <summary>
/// Response strings
Expand Down Expand Up @@ -220,6 +221,8 @@ static partial class CmdStrings
"ERR Invalid number of parameters to stored proc {0}, expected {1}, actual {2}";
public const string GenericSyntaxErrorOption = "ERR Syntax error in {0} option '{1}'";
public const string GenericParamShouldBeGreaterThanZero = "ERR {0} should be greater than 0";
public const string GenericErrCantBeNegative = "ERR {0} can't be negative";
public const string GenericErrShouldBeGreaterThanZero = "ERR {0} should be greater than 0";
public const string GenericUnknownClientType = "ERR Unknown client type '{0}'";
public const string GenericErrDuplicateFilter = "ERR Filter '{0}' defined multiple times";
public const string GenericPubSubCommandDisabled = "ERR {0} is disabled, enable it with --pubsub option.";
Expand Down
66 changes: 66 additions & 0 deletions libs/server/Resp/Objects/SetCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Diagnostics;
using System.Text;
using Garnet.common;
using Tsavorite.core;

Expand Down Expand Up @@ -151,6 +152,71 @@ private bool SetIntersectStore<TGarnetApi>(ref TGarnetApi storageApi)
return true;
}

/// <summary>
/// Returns the cardinality of the intersection between multiple sets.
/// </summary>
/// <param name="storageApi"></param>
/// <typeparam name="TGarnetApi"></typeparam>
/// <returns></returns>
private bool SetIntersectLength<TGarnetApi>(ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
if (parseState.Count < 2) // Need at least numkeys + 1 key
{
return AbortWithWrongNumberOfArguments(nameof(RespCommand.SINTERCARD));
}

// Number of keys
if (!parseState.TryGetInt(0, out var nKeys))
{
return AbortWithErrorMessage(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
}

if ((parseState.Count - 1) < nKeys)
{
return AbortWithErrorMessage(Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrShouldBeGreaterThanZero, "numkeys")));
}

var keys = parseState.Parameters.Slice(1, nKeys);

// Optional LIMIT argument
int? limit = null;
if (parseState.Count > nKeys + 1)
{
var limitArg = parseState.GetArgSliceByRef(nKeys + 1);
if (!limitArg.ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.LIMIT) || parseState.Count != nKeys + 3)
{
return AbortWithErrorMessage(CmdStrings.RESP_SYNTAX_ERROR);
}

if (!parseState.TryGetInt(nKeys + 2, out var limitVal))
{
return AbortWithErrorMessage(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
}

if (limitVal < 0)
{
return AbortWithErrorMessage(Encoding.ASCII.GetBytes(string.Format(CmdStrings.GenericErrCantBeNegative, "LIMIT")));
}

limit = limitVal;
}

var status = storageApi.SetIntersectLength(keys, limit, out var result);
switch (status)
{
case GarnetStatus.OK:
while (!RespWriteUtils.WriteInteger(result, ref dcurr, dend))
SendAndReset();
break;
case GarnetStatus.WRONGTYPE:
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend))
SendAndReset();
break;
}

return true;
}

/// <summary>
/// Returns the members of the set resulting from the union of all the given sets.
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 @@ -58,6 +58,7 @@ public enum RespCommand : ushort
SCARD,
SDIFF,
SINTER,
SINTERCARD,
SISMEMBER,
SMEMBERS,
SMISMEMBER,
Expand Down Expand Up @@ -1370,6 +1371,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan<byte>
{
return RespCommand.SMISMEMBER;
}
else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read<ulong>("10\r\nSINT"u8) && *(ulong*)(ptr + 9) == MemoryMarshal.Read<ulong>("ERCARD\r\n"u8))
{
return RespCommand.SINTERCARD;
}
else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read<ulong>("10\r\nZDIF"u8) && *(uint*)(ptr + 9) == MemoryMarshal.Read<uint>("FSTORE\r\n"u8))
{
return RespCommand.ZDIFFSTORE;
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 @@ -690,6 +690,7 @@ private bool ProcessArrayCommands<TGarnetApi>(RespCommand cmd, ref TGarnetApi st
RespCommand.SSCAN => ObjectScan(GarnetObjectType.Set, ref storageApi),
RespCommand.SMOVE => SetMove(ref storageApi),
RespCommand.SINTER => SetIntersect(ref storageApi),
RespCommand.SINTERCARD => SetIntersectLength(ref storageApi),
RespCommand.SINTERSTORE => SetIntersectStore(ref storageApi),
RespCommand.SUNION => SetUnion(ref storageApi),
RespCommand.SUNIONSTORE => SetUnionStore(ref storageApi),
Expand Down
48 changes: 47 additions & 1 deletion libs/server/Storage/Session/ObjectStore/SetOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ public GarnetStatus SetIntersectStore(byte[] key, ArgSlice[] keys, out int count
}


private GarnetStatus SetIntersect<TObjectContext>(ArgSlice[] keys, ref TObjectContext objectContext, out HashSet<byte[]> output)
private GarnetStatus SetIntersect<TObjectContext>(ReadOnlySpan<ArgSlice> keys, ref TObjectContext objectContext, out HashSet<byte[]> output)
where TObjectContext : ITsavoriteContext<byte[], IGarnetObject, ObjectInput, GarnetObjectStoreOutput, long, ObjectSessionFunctions, ObjectStoreFunctions, ObjectStoreAllocator>
{
output = new HashSet<byte[]>(ByteArrayComparer.Instance);
Expand Down Expand Up @@ -923,5 +923,51 @@ private GarnetStatus SetDiff<TObjectContext>(ArgSlice[] keys, ref TObjectContext

return GarnetStatus.OK;
}

/// <summary>
/// Returns the cardinality of the intersection of all the given sets.
/// </summary>
/// <param name="keys"></param>
/// <param name="limit">Optional limit for stopping early when reaching this size</param>
/// <param name="count"></param>
/// <returns></returns>
public GarnetStatus SetIntersectLength(ReadOnlySpan<ArgSlice> keys, int? limit, out int count)
{
if (txnManager.ObjectStoreLockableContext.Session is null)
ThrowObjectStoreUninitializedException();

count = 0;

if (keys.Length == 0)
return GarnetStatus.OK;

var createTransaction = false;

if (txnManager.state != TxnState.Running)
{
Debug.Assert(txnManager.state == TxnState.None);
createTransaction = true;
foreach (var item in keys)
txnManager.SaveKeyEntryToLock(item, true, LockType.Shared);
_ = txnManager.Run(true);
}

var setObjectStoreLockableContext = txnManager.ObjectStoreLockableContext;

try
{
var status = SetIntersect(keys, ref setObjectStoreLockableContext, out var result);
if (status == GarnetStatus.OK && result != null)
{
count = limit.HasValue ? Math.Min(result.Count, limit.Value) : result.Count;
}
return status;
}
finally
{
if (createTransaction)
txnManager.Commit(true);
}
}
}
}
1 change: 1 addition & 0 deletions playground/CommandInfoUpdater/SupportedCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ public class SupportedCommand
new("SUNION", RespCommand.SUNION),
new("SUNIONSTORE", RespCommand.SUNIONSTORE),
new("SINTER", RespCommand.SINTER),
new("SINTERCARD", RespCommand.SINTERCARD),
new("SINTERSTORE", RespCommand.SINTERSTORE),
new("TIME", RespCommand.TIME),
new("TTL", RespCommand.TTL),
Expand Down
29 changes: 29 additions & 0 deletions test/Garnet.test.cluster/RedirectTests/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,35 @@ public override ArraySegment<string>[] SetupSingleSlotRequest()
}
}

internal class SINTERCARD : BaseCommand
{
public override bool IsArrayCommand => true;
public override bool ArrayResponse => false;
public override string Command => nameof(SINTERCARD);

public override string[] GetSingleSlotRequest()
{
var ssk = GetSingleSlotKeys;
return ["3", ssk[0], ssk[1], ssk[2]];
}

public override string[] GetCrossSlotRequest()
{
var csk = GetCrossSlotKeys;
return ["3", csk[0], csk[1], csk[2]];
}

public override ArraySegment<string>[] SetupSingleSlotRequest()
{
var ssk = GetSingleSlotKeys;
var setup = new ArraySegment<string>[3];
setup[0] = new ArraySegment<string>(["SADD", ssk[1], "a", "b", "c"]);
setup[1] = new ArraySegment<string>(["SADD", ssk[2], "d", "e", "f"]);
setup[2] = new ArraySegment<string>(["SADD", ssk[3], "g", "h", "i"]);
return setup;
}
}

internal class SADD : BaseCommand
{
public override bool IsArrayCommand => false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public class ClusterSlotVerificationTests
new WATCH(),
new WATCHMS(),
new WATCHOS(),
new SINTERCARD(),
};

ClusterTestContext context;
Expand Down Expand Up @@ -305,6 +306,7 @@ public virtual void OneTimeTearDown()
[TestCase("WATCH")]
[TestCase("WATCHMS")]
[TestCase("WATCHOS")]
[TestCase("SINTERCARD")]
public void ClusterCLUSTERDOWNTest(string commandName)
{
var requestNodeIndex = otherIndex;
Expand Down Expand Up @@ -445,6 +447,7 @@ void GarnetClientSessionClusterDown(BaseCommand command)
[TestCase("CLUSTERSETPROC")]
[TestCase("WATCHMS")]
[TestCase("WATCHOS")]
[TestCase("SINTERCARD")]
public void ClusterOKTest(string commandName)
{
var requestNodeIndex = sourceIndex;
Expand Down Expand Up @@ -596,6 +599,7 @@ void GarnetClientSessionOK(BaseCommand command)
[TestCase("CLUSTERSETPROC")]
[TestCase("WATCHMS")]
[TestCase("WATCHOS")]
[TestCase("SINTERCARD")]
public void ClusterCROSSSLOTTest(string commandName)
{
var requestNodeIndex = sourceIndex;
Expand Down Expand Up @@ -739,6 +743,7 @@ void GarnetClientSessionCrossslotTest(BaseCommand command)
[TestCase("CLUSTERSETPROC")]
[TestCase("WATCHMS")]
[TestCase("WATCHOS")]
[TestCase("SINTERCARD")]
public void ClusterMOVEDTest(string commandName)
{
var requestNodeIndex = targetIndex;
Expand Down Expand Up @@ -889,6 +894,7 @@ void GarnetClientSessionMOVEDTest(BaseCommand command)
[TestCase("CLUSTERSETPROC")]
[TestCase("WATCHMS")]
[TestCase("WATCHOS")]
[TestCase("SINTERCARD")]
public void ClusterASKTest(string commandName)
{
var requestNodeIndex = sourceIndex;
Expand Down Expand Up @@ -1056,6 +1062,7 @@ void GarnetClientSessionASKTest(BaseCommand command)
[TestCase("CLUSTERSETPROC")]
[TestCase("WATCHMS")]
[TestCase("WATCHOS")]
[TestCase("SINTERCARD")]
public void ClusterTRYAGAINTest(string commandName)
{
var requestNodeIndex = sourceIndex;
Expand Down
Loading

0 comments on commit c07afca

Please sign in to comment.