Skip to content

Commit

Permalink
Add sample for multi-get where values match a specified prefix.
Browse files Browse the repository at this point in the history
  • Loading branch information
badrishc committed May 9, 2024
1 parent 844fedd commit f824a43
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 0 deletions.
23 changes: 23 additions & 0 deletions libs/server/Custom/CustomFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Buffers;
using System.Collections.Generic;
using Garnet.common;

namespace Garnet.server
Expand Down Expand Up @@ -76,6 +77,28 @@ protected static unsafe void WriteBulkStringArray(ref MemoryResult<byte> output,
}
}

/// <summary>
/// Create output as an array of bulk strings, from given array of ArgSlice values
/// </summary>
protected static unsafe void WriteBulkStringArray(ref MemoryResult<byte> output, List<ArgSlice> values)
{
int totalLen = 1 + NumUtils.NumDigits(values.Count) + 2;
for (int i = 0; i < values.Count; i++)
totalLen += RespWriteUtils.GetBulkStringLength(values[i].Length);

output.MemoryOwner?.Dispose();
output.MemoryOwner = MemoryPool.Rent(totalLen);
output.Length = totalLen;

fixed (byte* ptr = output.MemoryOwner.Memory.Span)
{
var curr = ptr;
RespWriteUtils.WriteArrayLength(values.Count, ref curr, ptr + totalLen);
for (int i = 0; i < values.Count; i++)
RespWriteUtils.WriteBulkString(values[i].Span, ref curr, ptr + totalLen);
}
}

/// <summary>
/// Create output as bulk string, from given Span
/// </summary>
Expand Down
62 changes: 62 additions & 0 deletions main/GarnetServer/Extensions/MGetIfPM.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using Garnet.common;
using Garnet.server;

namespace Garnet
{
/// <summary>
/// Functions to implement custom transaction MGETIFPM - get multiple keys whose values match with the given prefix
///
/// Format: MGETIFPM prefix key1 key2 ...
/// Output: array of matching key-value pairs
///
/// Description: Perform a non-transactional multi-get with value condition (prefix match) for the given set of keys
/// </summary>
sealed class MGetIfPM : CustomTransactionProcedure
{
/// <summary>
/// No transactional phase, skip Prepare
/// </summary>
public override bool Prepare<TGarnetReadApi>(TGarnetReadApi api, ArgSlice input)
=> false;

/// <summary>
/// Main will not be called because Prepare returns false
/// </summary>
public override void Main<TGarnetApi>(TGarnetApi api, ArgSlice input, ref MemoryResult<byte> output)
=> throw new InvalidOperationException();

/// <summary>
/// Perform the MGETIFPM operation
/// </summary>
public override void Finalize<TGarnetApi>(TGarnetApi api, ArgSlice input, ref MemoryResult<byte> output)
{
int offset = 0;

// Read prefix
var prefix = GetNextArg(input, ref offset);

// Read key, check condition, add to output
ArgSlice key;
List<ArgSlice> values = [];
while ((key = GetNextArg(input, ref offset)).Length > 0)
{
if (api.GET(key, out var value) == GarnetStatus.OK)
{
if (value.ReadOnlySpan.StartsWith(prefix.ReadOnlySpan))
{
values.Add(key);
values.Add(value);
}
}
}

// Return the matching key-value pairs as an array of bulk strings
WriteBulkStringArray(ref output, values);
}
}
}
3 changes: 3 additions & 0 deletions main/GarnetServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ static void RegisterExtensions(GarnetServer server)
// Register stored procedure to run a transactional command
server.Register.NewTransactionProc("MSETPX", () => new MSetPxTxn());

// Register stored procedure to run a transactional command
server.Register.NewTransactionProc("MGETIFPM", () => new MGetIfPM());

// Register stored procedure to run a non-transactional command
server.Register.NewTransactionProc("GETTWOKEYSNOTXN", 2, () => new GetTwoKeysNoTxn());

Expand Down
1 change: 1 addition & 0 deletions test/Garnet.test/Garnet.test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<Compile Include="..\..\main\GarnetServer\Extensions\DeleteIfMatch.cs" Link="DeleteIfMatch.cs" />
<Compile Include="..\..\main\GarnetServer\Extensions\MGetIfPM.cs" Link="MGetIfPM.cs" />
<Compile Include="..\..\main\GarnetServer\Extensions\MSetPx.cs" Link="MSetPx.cs" />
<Compile Include="..\..\main\GarnetServer\Extensions\MyDictObject.cs" Link="MyDictObject.cs" />
<Compile Include="..\..\main\GarnetServer\Extensions\GetTwoKeysNoTxn.cs" Link="GetTwoKeysNoTxn.cs" />
Expand Down
62 changes: 62 additions & 0 deletions test/Garnet.test/RespTransactionProcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,5 +411,67 @@ public void TransactionProcMSetPxTest()
Assert.IsNull(retValue);
}
}

[Test]
public void TransactionProcMGetIfPMTest()
{
server.Register.NewTransactionProc("MSETPX", () => new MSetPxTxn());
server.Register.NewTransactionProc("MGETIFPM", () => new MGetIfPM());

using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
var db = redis.GetDatabase(0);
const int NumKeys = 15;
const string prefix = "value1";

var args1 = new string[1 + 2 * NumKeys];

// Set expiry to 600 seconds
args1[0] = "600000";

// Set key-value pairs
for (int i = 0; i < NumKeys; i++)
{
args1[2 * i + 1] = $"key{i}";
args1[2 * i + 2] = $"value{i}";
}

// Execute transaction
var result1 = (string)db.Execute("MSETPX", args1);

// Verify transaction succeeded
Assert.AreEqual("OK", result1);

// Read keys to verify transaction succeeded
for (int i = 0; i < NumKeys; i++)
{
string key = $"key{i}";
string value = $"value{i}";
string retValue = db.StringGet(key);
Assert.AreEqual(value, retValue);
}

var args2 = new string[1 + NumKeys];

// Set prefix
args2[0] = prefix;

// Set keys
for (int i = 0; i < NumKeys; i++)
{
args2[i + 1] = $"key{i}";
}

// Execute transaction
var result2 = (string[])db.Execute("MGETIFPM", args2);

// Verify results
int expectedCount = NumKeys - 9; // only values with specified prefix
Assert.AreEqual(2 * expectedCount, result2.Length);
// Verify that keys have the correct prefix
for (int i = 0; i < expectedCount; i++)
{
Assert.AreEqual(prefix, result2[2 * i + 1].Substring(0, prefix.Length));
}
}
}
}

0 comments on commit f824a43

Please sign in to comment.