Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding basic version of DUMP and RESTORE commands #899

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2954d6e
Implement basic version of DUMP and RESTORE redis commands
s3w3nofficial Dec 22, 2024
0e7f821
Merge branch 'main' into s3w3nofficial/add-dump-and-restore-command
s3w3nofficial Jan 3, 2025
7406d74
refactor and add dump, restore to cluster slot verification test
s3w3nofficial Jan 12, 2025
8a9ee0a
Merge branch 'main' into s3w3nofficial/add-dump-and-restore-command
s3w3nofficial Jan 12, 2025
6133607
Update comments to use 'RESP' encoding terminology
badrishc Jan 13, 2025
c14f0b3
fix formating
s3w3nofficial Jan 15, 2025
0d18b6a
Merge branch 'main' into s3w3nofficial/add-dump-and-restore-command
s3w3nofficial Jan 15, 2025
2f9b457
fix tests
s3w3nofficial Jan 15, 2025
2fa9b08
add acl and tests and update default config to include the skip checksum
s3w3nofficial Jan 16, 2025
181d780
rm accidentally commited dump.rdb file
s3w3nofficial Jan 16, 2025
2a789d8
Remove trailing whitespace in RespCommandTests.cs
badrishc Jan 16, 2025
f15a662
fix comments
s3w3nofficial Jan 16, 2025
e281e34
run CommandInfoUpdater and replace docs / info files
s3w3nofficial Jan 16, 2025
d41421d
Merge branch 'main' into s3w3nofficial/add-dump-and-restore-command
s3w3nofficial Jan 16, 2025
737e7a9
Remove trailing whitespace in Options.cs
badrishc Jan 16, 2025
eed596f
fix RestoreACLsAsync test
s3w3nofficial Jan 16, 2025
85d0136
fix comments
s3w3nofficial Jan 16, 2025
2f2a9a3
optimize RespLengthEncodingUtils
s3w3nofficial Jan 17, 2025
024362b
implement suggestions
s3w3nofficial Jan 18, 2025
6b1b7b5
fix comments
s3w3nofficial Jan 19, 2025
60cb1e2
use SET_Conditional directly
s3w3nofficial Jan 19, 2025
02718c7
rename SkipChecksumValidation
s3w3nofficial Jan 19, 2025
1fb94a5
fix cluster restore test
s3w3nofficial Jan 19, 2025
52b1fa6
directly write to the output buffer for non-large objects
s3w3nofficial Jan 19, 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
82 changes: 82 additions & 0 deletions libs/common/Crc64.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;

namespace Garnet.common;

/// <summary>
/// Port of redis crc64 from https://github.com/redis/redis/blob/7.2/src/crc64.c
/// </summary>
public static class Crc64
{
/// <summary>
/// Polynomial (same as redis)
/// </summary>
private const ulong POLY = 0xad93d23594c935a9UL;

/// <summary>
/// Reverse all bits in a 64-bit value (bit reflection).
/// Only used for data_len == 64 in this code.
/// </summary>
private static ulong Reflect64(ulong data)
{
// swap odd/even bits
data = ((data >> 1) & 0x5555555555555555UL) | ((data & 0x5555555555555555UL) << 1);
// swap consecutive pairs
data = ((data >> 2) & 0x3333333333333333UL) | ((data & 0x3333333333333333UL) << 2);
// swap nibbles
data = ((data >> 4) & 0x0F0F0F0F0F0F0F0FUL) | ((data & 0x0F0F0F0F0F0F0F0FUL) << 4);
// swap bytes, then 2-byte pairs, then 4-byte pairs
data = System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(data);
return data;
}

/// <summary>
/// A direct bit-by-bit CRC64 calculation (like _crc64 in C).
/// </summary>
private static ulong Crc64Bitwise(ReadOnlySpan<byte> data)
{
ulong crc = 0;

foreach (var c in data)
{
for (byte i = 1; i != 0; i <<= 1)
{
// interpret the top bit of 'crc' and current bit of 'c'
var bitSet = (crc & 0x8000000000000000UL) != 0;
var cbit = (c & i) != 0;

// if cbit flips the sense, invert bitSet
if (cbit)
bitSet = !bitSet;

// shift
crc <<= 1;

// apply polynomial if needed
if (bitSet)
crc ^= POLY;
}

// ensure it stays in 64 bits
crc &= 0xffffffffffffffffUL;
}

// reflect and XOR, per standard
crc &= 0xffffffffffffffffUL;
crc = Reflect64(crc) ^ 0x0000000000000000UL;
return crc;
}

/// <summary>
/// Computes crc64
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static byte[] Hash(ReadOnlySpan<byte> data)
vazois marked this conversation as resolved.
Show resolved Hide resolved
{
var bitwiseCrc = Crc64Bitwise(data);
return BitConverter.GetBytes(bitwiseCrc);
}
}
82 changes: 82 additions & 0 deletions libs/common/RespLengthEncodingUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using System.Linq;

namespace Garnet.common;

/// <summary>
/// Utils for working with RESP length encoding
/// </summary>
public static class RespLengthEncodingUtils
{
/// <summary>
/// Decodes the RESP-encoded length and returns payload start
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static (long length, byte payloadStart) DecodeLength(ref ReadOnlySpan<byte> buff)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: C# convention is to PascalCase tuple members. So (long Length, byte PayloadStart).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{
// remove the value type byte
var encoded = buff.Slice(1);

if (encoded.Length == 0)
{
throw new ArgumentException("Encoded length cannot be empty.", nameof(encoded));
}

var firstByte = encoded[0];
return (firstByte >> 6) switch
{
// 6-bit encoding
0 => (firstByte & 0x3F, 1),
// 14-bit encoding
1 when encoded.Length < 2 => throw new ArgumentException("Not enough bytes for 14-bit encoding."),
1 => (((firstByte & 0x3F) << 8) | encoded[1], 2),
// 32-bit encoding
2 when encoded.Length < 5 => throw new ArgumentException("Not enough bytes for 32-bit encoding."),
2 => ((long)((encoded[1] << 24) | (encoded[2] << 16) | (encoded[3] << 8) | encoded[4]), 5),
_ => throw new ArgumentException("Invalid encoding type.", nameof(encoded))
};
}

/// <summary>
/// Encoded payload length to RESP-encoded payload length
/// </summary>
/// <param name="length"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static byte[] EncodeLength(long length)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole method is pretty allocate-y, which worries me if it's ever extensively used.

Copy link
Contributor

@PaulusParssinen PaulusParssinen Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree bit @kevin-montrose here. A common approach is to have small static method to first calculate the required buffer size (it can be simple switch expression) for the caller (i.e. in our case NetworkDUMP method) to know how much buffer it needs (to slice from existing buffer, stackalloc, rent etc., doesn't matter).

Then the actual encoding method takes a Span<byte> output argument which ownership belongs to caller. Now we could do the encoding on this buffer without allocations. You can see this pattern all over .NET with TryWrite and other encoding/serialization methods.

{
switch (length)
{
// 6-bit encoding (length ≤ 63)
case < 1 << 6:
return [(byte)(length & 0x3F)]; // 00xxxxxx
// 14-bit encoding (64 ≤ length ≤ 16,383)
case < 1 << 14:
{
var firstByte = (byte)(((length >> 8) & 0x3F) | (1 << 6)); // 01xxxxxx
var secondByte = (byte)(length & 0xFF);
return [firstByte, secondByte];
}
// 32-bit encoding (length ≤ 4,294,967,295)
case <= 0xFFFFFFFF:
{
var firstByte = (byte)(2 << 6); // 10xxxxxx
var lengthBytes = BitConverter.GetBytes((uint)length); // Ensure unsigned
if (BitConverter.IsLittleEndian)
{
Array.Reverse(lengthBytes); // Convert to big-endian
}

return [firstByte, .. lengthBytes];
}
default:
throw new ArgumentOutOfRangeException(
nameof(length), "Length exceeds maximum allowed for Redis encoding (4,294,967,295).");
}
}
}
5 changes: 5 additions & 0 deletions libs/host/Configuration/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,10 @@ internal sealed class Options
[Option("fail-on-recovery-error", Default = false, Required = false, HelpText = "Server bootup should fail if errors happen during bootup of AOF and checkpointing")]
public bool? FailOnRecoveryError { get; set; }

[OptionValidation]
[Option("skip-checksum-validation", Default = false, Required = false, HelpText = "Skip checksum validation")]
public bool? SkipChecksumValidation { get; set; }

[Option("lua-memory-management-mode", Default = LuaMemoryManagementMode.Native, Required = false, HelpText = "Memory management mode for Lua scripts, must be set to LimittedNative or Managed to impose script limits")]
public LuaMemoryManagementMode LuaMemoryManagementMode { get; set; }

Expand Down Expand Up @@ -728,6 +732,7 @@ public GarnetServerOptions GetServerOptions(ILogger logger = null)
IndexResizeThreshold = IndexResizeThreshold,
LoadModuleCS = LoadModuleCS,
FailOnRecoveryError = FailOnRecoveryError.GetValueOrDefault(),
SkipChecksumValidation = SkipChecksumValidation.GetValueOrDefault(),
LuaOptions = EnableLua.GetValueOrDefault() ? new LuaOptions(LuaMemoryManagementMode, LuaScriptMemoryLimit, logger) : null,
};
}
Expand Down
3 changes: 3 additions & 0 deletions libs/host/defaults.conf
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@
/* Fails if encounters error during AOF replay or checkpointing */
"FailOnRecoveryError": false,

/* Skips crc64 validation in restore command */
"SkipChecksumValidation": false,
Copy link
Contributor

@badrishc badrishc Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to SkipRDBRestoreChecksumValidation to clarify that this skip is specifically for the restore command (we have checksum validations in other unrelated places).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I named it like this because that's how it's named in the redis source code skip_checksum_validation. Also in the redis it only can be se as a build option and not in redis.conf.

Copy link
Contributor

@badrishc badrishc Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is fine to diverge from other resp caches in such respects. having a conf option is clearly more powerful, and we can name it in a way that makes sense for garnet.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


/* Lua uses the default, unmanaged and untracked, allocator */
"LuaMemoryManagementMode": "Native",

Expand Down
59 changes: 44 additions & 15 deletions libs/resources/RespCommandsDocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,6 @@
"Summary": "Pops an element from a list, pushes it to another list and returns it. Block until an element is available otherwise. Deletes the list if the last element was popped.",
"Group": "List",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060BLMOVE\u0060 with the \u0060RIGHT\u0060 and \u0060LEFT\u0060 arguments",
"Arguments": [
{
Expand Down Expand Up @@ -1427,7 +1426,6 @@
"Summary": "Returns the mapping of cluster slots to nodes.",
"Group": "Cluster",
"Complexity": "O(N) where N is the total number of Cluster nodes",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060CLUSTER SHARDS\u0060"
}
]
Expand Down Expand Up @@ -1654,6 +1652,22 @@
"Group": "Transactions",
"Complexity": "O(N), when N is the number of queued commands"
},
{
"Command": "DUMP",
"Name": "DUMP",
"Summary": "Returns a serialized representation of the value stored at a key.",
"Group": "Generic",
"Complexity": "O(1) to access the key and additional O(N*M) to serialize it, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)\u002BO(1*M) where M is small, so simply O(1).",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
},
{
"Command": "ECHO",
"Name": "ECHO",
Expand Down Expand Up @@ -2821,7 +2835,6 @@
"Summary": "Returns the previous string value of a key after setting it to a new value.",
"Group": "String",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060SET\u0060 with the \u0060!GET\u0060 argument",
"Arguments": [
{
Expand Down Expand Up @@ -3107,7 +3120,6 @@
"Summary": "Sets the values of multiple fields.",
"Group": "Hash",
"Complexity": "O(N) where N is the number of fields being set.",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060HSET\u0060 with multiple field-value pairs",
"Arguments": [
{
Expand Down Expand Up @@ -4452,7 +4464,6 @@
"Summary": "Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn\u0027t exist.",
"Group": "String",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060SET\u0060 with the \u0060PX\u0060 argument",
"Arguments": [
{
Expand Down Expand Up @@ -4635,7 +4646,6 @@
"Summary": "Closes the connection.",
"Group": "Connection",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "just closing the connection"
},
{
Expand Down Expand Up @@ -4878,6 +4888,34 @@
}
]
},
{
"Command": "RESTORE",
"Name": "RESTORE",
"Summary": "Creates a key from the serialized representation of a value.",
"Group": "Generic",
"Complexity": "O(1) to create the new key and additional O(N*M) to reconstruct the serialized value, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)\u002BO(1*M) where M is small, so simply O(1). However for sorted set values the complexity is O(N*M*log(N)) because inserting values into sorted sets is O(log(N)).",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "TTL",
"DisplayText": "ttl",
"Type": "Integer"
},
{
"TypeDiscriminator": "RespCommandBasicArgument",
"Name": "SERIALIZEDVALUE",
"DisplayText": "serialized-value",
"Type": "String"
}
]
},
{
"Command": "RPOP",
"Name": "RPOP",
Expand Down Expand Up @@ -4907,7 +4945,6 @@
"Summary": "Returns the last element of a list after removing and pushing it to another list. Deletes the list if the last element was popped.",
"Group": "List",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060LMOVE\u0060 with the \u0060RIGHT\u0060 and \u0060LEFT\u0060 arguments",
"Arguments": [
{
Expand Down Expand Up @@ -5400,7 +5437,6 @@
"Summary": "Sets the string value and expiration time of a key. Creates the key if it doesn\u0027t exist.",
"Group": "String",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060SET\u0060 with the \u0060EX\u0060 argument",
"Arguments": [
{
Expand Down Expand Up @@ -5480,7 +5516,6 @@
"Summary": "Set the string value of a key only when the key doesn\u0027t exist.",
"Group": "String",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060SET\u0060 with the \u0060NX\u0060 argument",
"Arguments": [
{
Expand Down Expand Up @@ -5626,7 +5661,6 @@
"Summary": "Sets a Redis server as a replica of another, or promotes it to being a master.",
"Group": "Server",
"Complexity": "O(1)",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060REPLICAOF\u0060",
"Arguments": [
{
Expand Down Expand Up @@ -5891,7 +5925,6 @@
"Summary": "Returns a substring from a string value.",
"Group": "String",
"Complexity": "O(N) where N is the length of the returned string. The complexity is ultimately determined by the returned length, but because creating a substring from an existing string is very cheap, it can be considered O(1) for small strings.",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060GETRANGE\u0060",
"Arguments": [
{
Expand Down Expand Up @@ -6763,7 +6796,6 @@
"Summary": "Returns members in a sorted set within a range of scores.",
"Group": "SortedSet",
"Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060ZRANGE\u0060 with the \u0060BYSCORE\u0060 argument",
"Arguments": [
{
Expand Down Expand Up @@ -7045,7 +7077,6 @@
"Summary": "Returns members in a sorted set within a range of indexes in reverse order.",
"Group": "SortedSet",
"Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements returned.",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060ZRANGE\u0060 with the \u0060REV\u0060 argument",
"Arguments": [
{
Expand Down Expand Up @@ -7083,7 +7114,6 @@
"Summary": "Returns members in a sorted set within a lexicographical range in reverse order.",
"Group": "SortedSet",
"Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060ZRANGE\u0060 with the \u0060REV\u0060 and \u0060BYLEX\u0060 arguments",
"Arguments": [
{
Expand Down Expand Up @@ -7134,7 +7164,6 @@
"Summary": "Returns members in a sorted set within a range of scores in reverse order.",
"Group": "SortedSet",
"Complexity": "O(log(N)\u002BM) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).",
"DocFlags": "Deprecated",
"ReplacedBy": "\u0060ZRANGE\u0060 with the \u0060REV\u0060 and \u0060BYSCORE\u0060 arguments",
"Arguments": [
{
Expand Down
Loading
Loading