Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9632985
Add limited support for inline command format.
prvyk Jun 29, 2025
c98c96f
This implementation will support all commands in inline command mode.
prvyk Jun 30, 2025
cbece8e
Continue simplifying.
prvyk Jul 1, 2025
a53788e
This implementation accepts some ridiculous strings valkey/redis woul…
prvyk Jul 3, 2025
3af0725
Consistent quoting + escaping
prvyk Jul 3, 2025
c7005f3
fmt
prvyk Jul 4, 2025
d27856a
Fixed failing tests.
prvyk Jul 4, 2025
53b5684
Handle root cause of earleir test failure. Checked against those test…
prvyk Jul 4, 2025
30df6c0
Handle GarnetObjectStoreDisabledError test.
prvyk Jul 4, 2025
5c585d8
MakeUpperCase is destructive, so escapes didn't work at command posit…
prvyk Jul 4, 2025
5d893cf
Add documentation
prvyk Jul 4, 2025
52014c4
The lone linefeed is allowed to be a command line terminator too.
prvyk Jul 6, 2025
40e77c5
Turns out carriage return is also recognized as a separator.
prvyk Jul 6, 2025
b361784
Correct comparison for separator past whitespace.
prvyk Jul 9, 2025
2b72adf
Exploit carriage return being a separator to simplify parsing code.
prvyk Jul 10, 2025
0b989c7
Due to escaping, the command slice could still be lowercased in inlin…
prvyk Jul 11, 2025
b4f6339
Add Windows documentation note
prvyk Jul 11, 2025
722aea2
Address some nits
prvyk Jul 15, 2025
87ab68d
Rework slow command parsing for commands with subcommands.
prvyk Jul 15, 2025
e99a3e5
Add ability to disable/enable inline commands using connection protec…
prvyk Jul 15, 2025
c6a82ea
fmt
prvyk Jul 15, 2025
1ce54a0
Revert "Rework slow command parsing for commands with subcommands."
prvyk Jul 15, 2025
cc8b0e1
Rework slow command parsing for commands with subcommands - this time…
prvyk Jul 15, 2025
ed24f5c
Use direct socket connection for inline tests.
prvyk Jul 16, 2025
cbd9271
Skip code should take into account that end-of-line is linefeed, not …
prvyk Jul 16, 2025
1c2bd5c
Add test for very short or incomplete inline command packages.
prvyk Jul 16, 2025
14c570b
Link to Redis inline docs.
prvyk Jul 16, 2025
4b96885
Revert "Skip code should take into account that end-of-line is linefe…
prvyk Jul 16, 2025
2811325
A longer comment explaining some issues with minimal packets and why …
prvyk Jul 16, 2025
88012d5
Fix test
prvyk Jul 29, 2025
2716865
Address nits
prvyk Jul 29, 2025
47ce3e4
Move functions to file where they're used.
prvyk Jul 29, 2025
3356cff
Reorder commands in slow parsing
prvyk Jul 30, 2025
5713c3a
Correct merge.
prvyk Aug 13, 2025
6e27518
Merge
prvyk Aug 19, 2025
0437f67
Merge branch 'main' into inlinecommands
prvyk Aug 26, 2025
1996224
Shifting inline code to function improves BDN performance.
prvyk Aug 26, 2025
50fb9a9
Merge branch 'main' into inlinecommands
prvyk Sep 2, 2025
9db384c
Merge branch 'main' into inlinecommands
prvyk Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions libs/client/ClientSession/GarnetClientSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
namespace Garnet.client
{
/// <summary>
/// Mono-threaded remote client session for Garnet (a session makes a single network connection, and
/// Mono-threaded remote client session for Garnet (a session makes a single network connection, and
/// expects mono-threaded client access, i.e., no concurrent invocations of API by client)
/// </summary>
public sealed partial class GarnetClientSession : IServerHook, IMessageConsumer
Expand Down Expand Up @@ -44,7 +44,7 @@ public sealed partial class GarnetClientSession : IServerHook, IMessageConsumer
Socket socket;
int disposed;

// Send
// Send
unsafe byte* offset, end;

// Num outstanding commands
Expand Down Expand Up @@ -103,6 +103,7 @@ public sealed partial class GarnetClientSession : IServerHook, IMessageConsumer
/// <param name="networkBufferSettings">Settings for send and receive network buffers</param>
/// <param name="networkPool">Buffer pool to use for allocating send and receive buffers</param>
/// <param name="networkSendThrottleMax">Max outstanding network sends allowed</param>
/// <param name="rawResult">Recieve result as raw string</param>
/// <param name="logger">Logger</param>
public GarnetClientSession(
EndPoint endpoint,
Expand Down Expand Up @@ -431,6 +432,7 @@ public void Wait()
private unsafe void InternalExecute(params string[] command)
{
byte* curr = offset;

while (!RespWriteUtils.TryWriteArrayLength(command.Length, ref curr, end))
{
Flush();
Expand Down
16 changes: 16 additions & 0 deletions libs/common/AsciiUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ public static bool IsBetween(byte c, char minInclusive, char maxInclusive)
return (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive);
}

/// <summary>Indicates whether character is ASCII quote character.</summary>
/// <param name="c">The character to evaluate.</param>
/// <returns>true if <paramref name="c"/> is an ASCII quote character; otherwise, false.</returns>
public static bool IsQuoteChar(byte c)
{
return (c == '"') || (c == '\'');
}

/// <summary>Indicates whether character is ASCII whitespace or a carriage return.</summary>
/// <param name="c">The character to evaluate.</param>
/// <returns>true if <paramref name="c"/> is an ASCII whitespace character; otherwise, false.</returns>
public static bool IsRedisWhiteSpace(byte c)
{
return (c == ' ') || (c == '\t') || (c == '\r');
}

public static byte ToLower(byte c)
{
if (IsBetween(c, 'A', 'Z'))
Expand Down
5 changes: 5 additions & 0 deletions libs/host/Configuration/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,10 @@ internal sealed class Options : ICloneable
[Option("enable-debug-command", Required = false, HelpText = "Enable DEBUG command for 'no', 'local' or 'all' connections")]
public ConnectionProtectionOption EnableDebugCommand { get; set; }

[OptionValidation]
[Option("enable-inline-command", Required = false, HelpText = "Enable inline commands for 'no', 'local' or 'all' connections")]
public ConnectionProtectionOption EnableInlineCommands { get; set; }

[OptionValidation]
[Option("enable-module-command", Required = false, HelpText = "Enable MODULE command for 'no', 'local' or 'all' connections. Command can only load from paths listed in ExtensionBinPaths")]
public ConnectionProtectionOption EnableModuleCommand { get; set; }
Expand Down Expand Up @@ -922,6 +926,7 @@ public GarnetServerOptions GetServerOptions(ILogger logger = null)
RevivInChainOnly = RevivInChainOnly.GetValueOrDefault(),
RevivObjBinRecordCount = RevivObjBinRecordCount,
EnableDebugCommand = EnableDebugCommand,
EnableInlineCommands = EnableInlineCommands,
EnableModuleCommand = EnableModuleCommand,
ExtensionBinPaths = FileUtils.ConvertToAbsolutePaths(ExtensionBinPaths),
ExtensionAllowUnsignedAssemblies = ExtensionAllowUnsignedAssemblies.GetValueOrDefault(),
Expand Down
5 changes: 4 additions & 1 deletion libs/host/defaults.conf
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,10 @@
/* Enable DEBUG command for clients - no/local/yes */
"EnableDebugCommand": "no",

/* Enable DEBUG command for clients - no/local/yes */
/* Enable inline commands for clients - no/local/yes */
"EnableInlineCommands": "local",

/* Enable MODULE command for clients - no/local/yes */
"EnableModuleCommand": "no",

/* Protected mode */
Expand Down
151 changes: 151 additions & 0 deletions libs/server/ArgSlice/ArgSliceUtils.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using Garnet.common;

namespace Garnet.server
Expand All @@ -10,10 +11,160 @@ namespace Garnet.server
/// </summary>
public static class ArgSliceUtils
{
private static ReadOnlySpan<byte> CharToHexLookup =>
[
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63
0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95
0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
];

/// <summary>
/// Compute hash slot of given ArgSlice
/// </summary>
public static unsafe ushort HashSlot(ref ArgSlice argSlice)
=> HashSlotUtils.HashSlot(argSlice.ptr, argSlice.Length);

/// <summary>
/// Takes a quoted string from given ArgSlice and unescapes it. Destructive: Writes over the input memory.
/// </summary>
/// <param name="slice"></param>
/// <param name="acceptUppercaseEscapes"></param>
/// <remarks>See TryParseInlineCommandArguments() for the quoting/escaping rules</remarks>
/// <returns></returns>
public static unsafe ArgSlice Unescape(ArgSlice slice, bool acceptUppercaseEscapes = false)
{
// Too short for quoting.
if (slice.Length <= 1)
{
return slice;
}

// Get the last character to know if this is a quoted context.
var type = slice.Span[slice.Length - 1];

// Nothing to do if it's not.
if (!AsciiUtils.IsQuoteChar(type))
{
return slice;
}

// It's a quoted context, so we need to check for escapes.

// Too short for escaping
if (slice.Length <= 3)
{
return new ArgSlice(slice.ptr + 1, slice.Length - 2);
}

// How many bytes do we need to shift thanks to quoting and escaping.
var shift = 0;
// start offset
var start = 0;
// Are we in a quoting context?
var qStart = false;

// Optimize command case by changing our start point instead of shifting the entire string.
if (slice.Span[0] == type)
{
qStart = true;
start = 1;
}

for (var j = start; j < slice.Span.Length - shift - 1; ++j)
{
if (!qStart)
{
if (slice.Span[j + shift] == type)
{
qStart = true;
shift++;
slice.Span[j] = slice.Span[j + 1];
}
continue;
}

if (slice.Span[j + shift] != '\\')
{
// If we shifted earlier, we need to shift the rest too.
if (shift > 0)
slice.Span[j] = slice.Span[j + shift];
continue;
}

// ' context recognize only this particular sequence
if (type == '\'')
{
if (slice.Span[j + shift + 1] == '\'')
shift++;
if (shift > 0)
slice.Span[j] = slice.Span[j + shift];
continue;
}

// Process escapes
shift++;
var c = acceptUppercaseEscapes ?
(char)AsciiUtils.ToLower(slice.Span[j + shift]) :
(char)slice.Span[j + shift];

switch (c)
{
case 'a':
c = '\a';
break;
case 'b':
c = '\b';
break;
case 'n':
c = '\n';
break;
case 'r':
c = '\r';
break;
case 't':
c = '\t';
break;
case 'x':
if (j + shift + 2 < slice.Span.Length)
{
var val =
16 * CharToHexLookup[slice.ReadOnlySpan[j + shift + 1]] +
CharToHexLookup[slice.ReadOnlySpan[j + shift + 2]];

if (val < 0xFF)
{
c = (char)val;
shift += 2;
}
break;
}
break;
default:
break;
}

slice.Span[j] = (byte)c;
}

// Zero unnecessary chars
slice.Span[(slice.Span.Length - shift - start)..(slice.Span.Length - 1)].Clear();

// The final cut must remove 1 from length to trim the end quote character.
// (The starting quote character was already shifted out)
return new ArgSlice(slice.ptr + start, slice.Span.Length - 1 - shift - start);
}
}
}
4 changes: 2 additions & 2 deletions libs/server/Resp/BasicCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1751,8 +1751,8 @@ private static void WriteClientInfo(IClusterProvider provider, StringBuilder int
bool ParseGETAndKey(ref SpanByte key)
{
var oldEndReadHead = readHead = endReadHead;
var cmd = ParseCommand(writeErrorOnFailure: true, out var success);
if (!success || cmd != RespCommand.GET)
var cmd = ParseCommand(writeErrorOnFailure: true, out var commandReceived);
if (!commandReceived || cmd != RespCommand.GET)
{
// If we either find no command or a different command, we back off
endReadHead = readHead = oldEndReadHead;
Expand Down
3 changes: 2 additions & 1 deletion libs/server/Resp/CmdStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ static partial class CmdStrings
/// </summary>
public static ReadOnlySpan<byte> RESP_ERR_GENERIC_UNK_CMD => "ERR unknown command"u8;
public static ReadOnlySpan<byte> RESP_ERR_NOT_SUPPORTED_RESP2 => "ERR command not supported in RESP2"u8;
public static ReadOnlySpan<byte> RESP_ERR_UNBALANCED_QUOTES => "ERR Protocol error: unbalanced quotes in request"u8;
public static ReadOnlySpan<byte> RESP_ERR_GENERIC_CLUSTER_DISABLED => "ERR This instance has cluster support disabled"u8;
public static ReadOnlySpan<byte> RESP_ERR_LUA_DISABLED => "ERR This instance has Lua scripting support disabled"u8;
public static ReadOnlySpan<byte> RESP_ERR_GENERIC_WRONG_ARGUMENTS => "ERR wrong number of arguments for 'config|set' command"u8;
Expand Down Expand Up @@ -428,7 +429,7 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> NO => "NO"u8;

// Cluster subcommands which are internal and thus undocumented
//
//
// Because these are internal, they have lower case property names
public static ReadOnlySpan<byte> gossip => "GOSSIP"u8;
public static ReadOnlySpan<byte> myparentid => "MYPARENTID"u8;
Expand Down
Loading
Loading