Skip to content

Commit 7483efc

Browse files
Adding basic version of DUMP and RESTORE commands (#899)
* Implement basic version of DUMP and RESTORE redis commands * refactor and add dump, restore to cluster slot verification test * Update comments to use 'RESP' encoding terminology * fix formating * fix tests * add acl and tests and update default config to include the skip checksum validation flag * rm accidentally commited dump.rdb file * Remove trailing whitespace in RespCommandTests.cs * fix comments * run CommandInfoUpdater and replace docs / info files * Remove trailing whitespace in Options.cs * fix RestoreACLsAsync test * fix comments * optimize RespLengthEncodingUtils * implement suggestions * fix comments * use SET_Conditional directly * rename SkipChecksumValidation * fix cluster restore test * directly write to the output buffer for non-large objects * Refactor WriteDirect call in KeyAdminCommands * Mark multiple commands as deprecated in JSON * Mark commands as deprecated in documentation --------- Co-authored-by: Badrish Chandramouli <[email protected]>
1 parent c85e281 commit 7483efc

18 files changed

+993
-8
lines changed

libs/common/Crc64.cs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
6+
namespace Garnet.common;
7+
8+
/// <summary>
9+
/// Port of redis crc64 from https://github.com/redis/redis/blob/7.2/src/crc64.c
10+
/// </summary>
11+
public static class Crc64
12+
{
13+
/// <summary>
14+
/// Polynomial (same as redis)
15+
/// </summary>
16+
private const ulong POLY = 0xad93d23594c935a9UL;
17+
18+
/// <summary>
19+
/// Reverse all bits in a 64-bit value (bit reflection).
20+
/// Only used for data_len == 64 in this code.
21+
/// </summary>
22+
private static ulong Reflect64(ulong data)
23+
{
24+
// swap odd/even bits
25+
data = ((data >> 1) & 0x5555555555555555UL) | ((data & 0x5555555555555555UL) << 1);
26+
// swap consecutive pairs
27+
data = ((data >> 2) & 0x3333333333333333UL) | ((data & 0x3333333333333333UL) << 2);
28+
// swap nibbles
29+
data = ((data >> 4) & 0x0F0F0F0F0F0F0F0FUL) | ((data & 0x0F0F0F0F0F0F0F0FUL) << 4);
30+
// swap bytes, then 2-byte pairs, then 4-byte pairs
31+
data = System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(data);
32+
return data;
33+
}
34+
35+
/// <summary>
36+
/// A direct bit-by-bit CRC64 calculation (like _crc64 in C).
37+
/// </summary>
38+
private static ulong Crc64Bitwise(ReadOnlySpan<byte> data)
39+
{
40+
ulong crc = 0;
41+
42+
foreach (var c in data)
43+
{
44+
for (byte i = 1; i != 0; i <<= 1)
45+
{
46+
// interpret the top bit of 'crc' and current bit of 'c'
47+
var bitSet = (crc & 0x8000000000000000UL) != 0;
48+
var cbit = (c & i) != 0;
49+
50+
// if cbit flips the sense, invert bitSet
51+
if (cbit)
52+
bitSet = !bitSet;
53+
54+
// shift
55+
crc <<= 1;
56+
57+
// apply polynomial if needed
58+
if (bitSet)
59+
crc ^= POLY;
60+
}
61+
62+
// ensure it stays in 64 bits
63+
crc &= 0xffffffffffffffffUL;
64+
}
65+
66+
// reflect and XOR, per standard
67+
crc &= 0xffffffffffffffffUL;
68+
crc = Reflect64(crc) ^ 0x0000000000000000UL;
69+
return crc;
70+
}
71+
72+
/// <summary>
73+
/// Computes crc64
74+
/// </summary>
75+
/// <param name="data"></param>
76+
/// <returns></returns>
77+
public static byte[] Hash(ReadOnlySpan<byte> data)
78+
{
79+
var bitwiseCrc = Crc64Bitwise(data);
80+
return BitConverter.GetBytes(bitwiseCrc);
81+
}
82+
}
+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.Buffers.Binary;
6+
7+
namespace Garnet.common;
8+
9+
/// <summary>
10+
/// Utils for working with RESP length encoding
11+
/// </summary>
12+
public static class RespLengthEncodingUtils
13+
{
14+
/// <summary>
15+
/// Maximum length that can be encoded
16+
/// </summary>
17+
private const int MaxLength = 0xFFFFFF;
18+
19+
/// <summary>
20+
/// Try read RESP-encoded length
21+
/// </summary>
22+
/// <param name="input"></param>
23+
/// <param name="length"></param>
24+
/// <param name="bytesRead"></param>
25+
/// <returns></returns>
26+
public static bool TryReadLength(ReadOnlySpan<byte> input, out int length, out int bytesRead)
27+
{
28+
length = 0;
29+
bytesRead = 0;
30+
if (input.Length < 1)
31+
{
32+
return false;
33+
}
34+
35+
var firstByte = input[0];
36+
switch (firstByte >> 6)
37+
{
38+
case 0:
39+
bytesRead = 1;
40+
length = firstByte & 0x3F;
41+
return true;
42+
case 1 when input.Length > 1:
43+
bytesRead = 2;
44+
length = ((firstByte & 0x3F) << 8) | input[1];
45+
return true;
46+
case 2:
47+
bytesRead = 5;
48+
return BinaryPrimitives.TryReadInt32BigEndian(input, out length);
49+
default:
50+
return false;
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Try to write RESP-encoded length
56+
/// </summary>
57+
/// <param name="length"></param>
58+
/// <param name="output"></param>
59+
/// <param name="bytesWritten"></param>
60+
/// <returns></returns>
61+
public static bool TryWriteLength(int length, Span<byte> output, out int bytesWritten)
62+
{
63+
bytesWritten = 0;
64+
65+
if (length > MaxLength)
66+
{
67+
return false;
68+
}
69+
70+
// 6-bit encoding (length ≤ 63)
71+
if (length < 1 << 6)
72+
{
73+
if (output.Length < 1)
74+
{
75+
return false;
76+
}
77+
78+
output[0] = (byte)(length & 0x3F);
79+
80+
bytesWritten = 1;
81+
return true;
82+
}
83+
84+
// 14-bit encoding (64 ≤ length ≤ 16,383)
85+
if (length < 1 << 14)
86+
{
87+
if (output.Length < 2)
88+
{
89+
return false;
90+
}
91+
92+
output[0] = (byte)(((length >> 8) & 0x3F) | (1 << 6));
93+
output[1] = (byte)(length & 0xFF);
94+
95+
bytesWritten = 2;
96+
return true;
97+
}
98+
99+
// 32-bit encoding (length ≤ 4,294,967,295)
100+
if (output.Length < 5)
101+
{
102+
return false;
103+
}
104+
105+
output[0] = 2 << 6;
106+
BinaryPrimitives.WriteUInt32BigEndian(output.Slice(1), (uint)length);
107+
108+
bytesWritten = 5;
109+
return true;
110+
}
111+
}

libs/host/Configuration/Options.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

44
using System;
@@ -512,6 +512,10 @@ internal sealed class Options
512512
[Option("fail-on-recovery-error", Default = false, Required = false, HelpText = "Server bootup should fail if errors happen during bootup of AOF and checkpointing")]
513513
public bool? FailOnRecoveryError { get; set; }
514514

515+
[OptionValidation]
516+
[Option("skip-rdb-restore-checksum-validation", Default = false, Required = false, HelpText = "Skip RDB restore checksum validation")]
517+
public bool? SkipRDBRestoreChecksumValidation { get; set; }
518+
515519
[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")]
516520
public LuaMemoryManagementMode LuaMemoryManagementMode { get; set; }
517521

@@ -732,6 +736,7 @@ public GarnetServerOptions GetServerOptions(ILogger logger = null)
732736
IndexResizeThreshold = IndexResizeThreshold,
733737
LoadModuleCS = LoadModuleCS,
734738
FailOnRecoveryError = FailOnRecoveryError.GetValueOrDefault(),
739+
SkipRDBRestoreChecksumValidation = SkipRDBRestoreChecksumValidation.GetValueOrDefault(),
735740
LuaOptions = EnableLua.GetValueOrDefault() ? new LuaOptions(LuaMemoryManagementMode, LuaScriptMemoryLimit, logger) : null,
736741
};
737742
}

libs/host/defaults.conf

+3
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@
347347
/* Fails if encounters error during AOF replay or checkpointing */
348348
"FailOnRecoveryError": false,
349349

350+
/* Skips crc64 validation in restore command */
351+
"SkipRDBRestoreChecksumValidation": false,
352+
350353
/* Lua uses the default, unmanaged and untracked, allocator */
351354
"LuaMemoryManagementMode": "Native",
352355

libs/resources/RespCommandsDocs.json

+44
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,22 @@
16981698
"Group": "Transactions",
16991699
"Complexity": "O(N), when N is the number of queued commands"
17001700
},
1701+
{
1702+
"Command": "DUMP",
1703+
"Name": "DUMP",
1704+
"Summary": "Returns a serialized representation of the value stored at a key.",
1705+
"Group": "Generic",
1706+
"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).",
1707+
"Arguments": [
1708+
{
1709+
"TypeDiscriminator": "RespCommandKeyArgument",
1710+
"Name": "KEY",
1711+
"DisplayText": "key",
1712+
"Type": "Key",
1713+
"KeySpecIndex": 0
1714+
}
1715+
]
1716+
},
17011717
{
17021718
"Command": "ECHO",
17031719
"Name": "ECHO",
@@ -5429,6 +5445,34 @@
54295445
}
54305446
]
54315447
},
5448+
{
5449+
"Command": "RESTORE",
5450+
"Name": "RESTORE",
5451+
"Summary": "Creates a key from the serialized representation of a value.",
5452+
"Group": "Generic",
5453+
"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)).",
5454+
"Arguments": [
5455+
{
5456+
"TypeDiscriminator": "RespCommandKeyArgument",
5457+
"Name": "KEY",
5458+
"DisplayText": "key",
5459+
"Type": "Key",
5460+
"KeySpecIndex": 0
5461+
},
5462+
{
5463+
"TypeDiscriminator": "RespCommandBasicArgument",
5464+
"Name": "TTL",
5465+
"DisplayText": "ttl",
5466+
"Type": "Integer"
5467+
},
5468+
{
5469+
"TypeDiscriminator": "RespCommandBasicArgument",
5470+
"Name": "SERIALIZEDVALUE",
5471+
"DisplayText": "serialized-value",
5472+
"Type": "String"
5473+
}
5474+
]
5475+
},
54325476
{
54335477
"Command": "RPOP",
54345478
"Name": "RPOP",

libs/resources/RespCommandsInfo.json

+50
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,31 @@
10481048
"Flags": "Fast, Loading, NoScript, Stale, AllowBusy",
10491049
"AclCategories": "Fast, Transaction"
10501050
},
1051+
{
1052+
"Command": "DUMP",
1053+
"Name": "DUMP",
1054+
"Arity": 2,
1055+
"Flags": "ReadOnly",
1056+
"FirstKey": 1,
1057+
"LastKey": 1,
1058+
"Step": 1,
1059+
"AclCategories": "KeySpace, Read",
1060+
"KeySpecifications": [
1061+
{
1062+
"BeginSearch": {
1063+
"TypeDiscriminator": "BeginSearchIndex",
1064+
"Index": 1
1065+
},
1066+
"FindKeys": {
1067+
"TypeDiscriminator": "FindKeysRange",
1068+
"LastKey": 0,
1069+
"KeyStep": 1,
1070+
"Limit": 0
1071+
},
1072+
"Flags": "RO, Access"
1073+
}
1074+
]
1075+
},
10511076
{
10521077
"Command": "ECHO",
10531078
"Name": "ECHO",
@@ -3403,6 +3428,31 @@
34033428
"Flags": "Admin, NoAsyncLoading, NoScript, Stale",
34043429
"AclCategories": "Admin, Dangerous, Slow"
34053430
},
3431+
{
3432+
"Command": "RESTORE",
3433+
"Name": "RESTORE",
3434+
"Arity": -4,
3435+
"Flags": "DenyOom, Write",
3436+
"FirstKey": 1,
3437+
"LastKey": 1,
3438+
"Step": 1,
3439+
"AclCategories": "Dangerous, KeySpace, Slow, Write",
3440+
"KeySpecifications": [
3441+
{
3442+
"BeginSearch": {
3443+
"TypeDiscriminator": "BeginSearchIndex",
3444+
"Index": 1
3445+
},
3446+
"FindKeys": {
3447+
"TypeDiscriminator": "FindKeysRange",
3448+
"LastKey": 0,
3449+
"KeyStep": 0,
3450+
"Limit": 0
3451+
},
3452+
"Flags": "OW, Update"
3453+
}
3454+
]
3455+
},
34063456
{
34073457
"Command": "RPOP",
34083458
"Name": "RPOP",

libs/server/Resp/CmdStrings.cs

+1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ static partial class CmdStrings
231231
public static ReadOnlySpan<byte> RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8;
232232
public static ReadOnlySpan<byte> RESP_ERR_INVALID_BITFIELD_TYPE => "ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is"u8;
233233
public static ReadOnlySpan<byte> RESP_ERR_SCRIPT_FLUSH_OPTIONS => "ERR SCRIPT FLUSH only support SYNC|ASYNC option"u8;
234+
public static ReadOnlySpan<byte> RESP_ERR_BUSSYKEY => "BUSYKEY Target key name already exists."u8;
234235
public static ReadOnlySpan<byte> RESP_ERR_LENGTH_AND_INDEXES => "If you want both the length and indexes, please just use IDX."u8;
235236
public static ReadOnlySpan<byte> RESP_ERR_INVALID_EXPIRE_TIME => "ERR invalid expire time, must be >= 0"u8;
236237
public static ReadOnlySpan<byte> RESP_ERR_HCOLLECT_ALREADY_IN_PROGRESS => "ERR HCOLLECT scan already in progress"u8;

0 commit comments

Comments
 (0)