From ce7d671f9da4ec6108a37deae2a8a1a5e2740136 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 17 Sep 2024 18:31:12 -0700 Subject: [PATCH 01/87] init etag impl --- libs/server/API/GarnetApi.cs | 7 +- libs/server/API/GarnetStatus.cs | 6 +- libs/server/API/GarnetWatchApi.cs | 7 + libs/server/API/IGarnetApi.cs | 7 +- libs/server/Resp/BasicCommands.cs | 200 ++++++++++++- libs/server/Resp/CmdStrings.cs | 2 + libs/server/Resp/Parser/RespCommand.cs | 21 +- libs/server/Resp/RespServerSession.cs | 4 + .../Functions/MainStore/PrivateMethods.cs | 134 ++++++--- .../Storage/Functions/MainStore/RMWMethods.cs | 277 +++++++++++++----- .../Functions/MainStore/ReadMethods.cs | 72 ++++- .../Functions/MainStore/VarLenInputMethods.cs | 1 + .../Storage/Session/MainStore/MainStoreOps.cs | 65 ++++ .../cs/src/core/Index/Common/RecordInfo.cs | 20 +- .../Tsavorite/Implementation/InternalRMW.cs | 3 + .../Tsavorite/cs/src/core/Utilities/Status.cs | 5 + .../Tsavorite/cs/src/core/VarLen/SpanByte.cs | 18 +- test/Garnet.test/RespEtagTests.cs | 173 +++++++++++ test/Garnet.test/RespTests.cs | 135 +++++++++ 19 files changed, 1004 insertions(+), 153 deletions(-) create mode 100644 test/Garnet.test/RespEtagTests.cs diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index 2217e662db..66dd0fba77 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -51,6 +51,9 @@ public void WATCH(byte[] key, StoreType type) public GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) => storageSession.GET(ref key, ref input, ref output, ref context); + public GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) + => storageSession.GETForETagCmd(ref key, ref input, ref output, ref context); + /// public GarnetStatus GET_WithPending(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, long ctx, out bool pending) => storageSession.GET_WithPending(ref key, ref input, ref output, ctx, out pending, ref context); @@ -103,8 +106,8 @@ public GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input) => storageSession.SET_Conditional(ref key, ref input, ref context); /// - public GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) - => storageSession.SET_Conditional(ref key, ref input, ref output, ref context); + public GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, RespCommand cmd) + => storageSession.SET_Conditional(ref key, ref input, ref output, ref context, cmd); /// public GarnetStatus SET(ArgSlice key, Memory value) diff --git a/libs/server/API/GarnetStatus.cs b/libs/server/API/GarnetStatus.cs index 2277461ad4..54d038c4ec 100644 --- a/libs/server/API/GarnetStatus.cs +++ b/libs/server/API/GarnetStatus.cs @@ -23,6 +23,10 @@ public enum GarnetStatus : byte /// /// Wrong type /// - WRONGTYPE + WRONGTYPE, + /// + /// ETAG mismatch result for an etag based command + /// + ETAGMISMATCH, } } \ No newline at end of file diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index bbae63343a..d93bc9ff97 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -29,6 +29,13 @@ public GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref SpanByteAndMem return garnetApi.GET(ref key, ref input, ref output); } + public GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) + { + garnetApi.WATCH(new ArgSlice(ref key), StoreType.Main); + return garnetApi.GETForETagCmd(ref key, ref input, ref output); + } + + /// public GarnetStatus GETForMemoryResult(ArgSlice key, out MemoryResult value) { diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 7527d35865..d8c9b30a6d 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -27,7 +27,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// SET Conditional /// - GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output); + GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, RespCommand cmd); /// /// SET @@ -1000,6 +1000,11 @@ public interface IGarnetReadApi /// GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output); + /// + /// GET + /// + GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output); + /// /// GET /// diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index c2cf817ad6..6b36a4041e 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -268,6 +268,164 @@ private bool NetworkSET(ref TGarnetApi storageApi) return true; } + /// + /// GETWITHETAG key + /// Given a key get the value and ETag + /// + private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + Debug.Assert(parseState.Count == 1); + + var key = parseState.GetArgSliceByRef(0).SpanByte; + var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + + // Setup input buffer + SpanByte input = SpanByte.Reinterpret(stackalloc byte[NumUtils.MaximumFormatInt64Length]); + byte* inputPtr = input.ToPointer(); + ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; + ((RespInputHeader*)inputPtr)->flags = 0; + + var status = storageApi.GETForETagCmd(ref key, ref input, ref output); + + // Get the ETAG and Value and start typing + switch (status) + { + case GarnetStatus.NOTFOUND: + Debug.Assert(output.IsSpanByte); + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + default: + if (!output.IsSpanByte) + SendAndReset(output.Memory, output.Length); + else + dcurr += output.Length; + break; + } + + return true; + } + + /// + /// GETIFNOTMATCH key etag + /// Given a key and an etag, return the value and it's etag only if the sent ETag does not match the existing ETag + /// If the ETag matches then we just send back a string indicating the value has not changed. + /// + private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + Debug.Assert(parseState.Count == 2); + + var key = parseState.GetArgSliceByRef(0).SpanByte; + var etagToCheckWith = parseState.GetLong(1); + + var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + + // Setup input buffer to pass command info, and the ETag to check with. + SpanByte input = SpanByte.Reinterpret(stackalloc byte[NumUtils.MaximumFormatInt64Length]); + byte* inputPtr = input.ToPointer(); + ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETIFNOTMATCH; + ((RespInputHeader*)inputPtr)->flags = 0; + *(long*)(inputPtr + RespInputHeader.Size) = etagToCheckWith; + + var status = storageApi.GETForETagCmd(ref key, ref input, ref output); + + switch (status) + { + case GarnetStatus.NOTFOUND: + Debug.Assert(output.IsSpanByte); + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + default: + if (!output.IsSpanByte) + SendAndReset(output.Memory, output.Length); + else + dcurr += output.Length; + break; + } + + return true; + } + + /// + /// SETIFNOTMATCH key val etag + /// Sets a key value pair only if an already existing etag does not match the etag sent as a part of the request + /// + /// + /// + /// + private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + Debug.Assert(parseState.Count == 3); + + var key = parseState.GetArgSliceByRef(0).SpanByte; + var value = parseState.GetArgSliceByRef(1).SpanByte; + var etagToCheckWith = parseState.GetLong(2); + + // Move value forward to make space for ETAG in Value itself + var initialValueSize = value.Length; + value.Length += sizeof(long); + var valPtr = value.ToPointer(); + Buffer.MemoryCopy(valPtr, sizeof(long) + valPtr, initialValueSize, initialValueSize); + // now insert the ETag at the start of the valPtr + *(long*)valPtr = etagToCheckWith; + + // Make space for key header + var keyPtr = key.ToPointer() - sizeof(int); + // Set key length + *(int*)keyPtr = key.Length; + + NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, valPtr - sizeof(int), value.Length, true, false, ref storageApi); + + return true; + } + + /// + /// SETWITHETAG key val + /// Sets a key value pair with an ETAG associated with the value internally + /// Calling this on a key that already exists is an error case + /// + private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + Debug.Assert(parseState.Count == 2); + + var key = parseState.GetArgSliceByRef(0).SpanByte; + var value = parseState.GetArgSliceByRef(1).SpanByte; + + // Move value forward to make space for ETAG in Value + var initialValueSize = value.Length; + value.Length += sizeof(long); + var valPtr = value.ToPointer(); + Buffer.MemoryCopy(valPtr, sizeof(long) + valPtr, initialValueSize, initialValueSize); + // now insert the ETag at the start of the valPtr + long initialEtag = 0; + *(long*)valPtr = initialEtag; + + // Make space for key header + var keyPtr = key.ToPointer() - sizeof(int); + // Set key length + *(int*)keyPtr = key.Length; + + NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, valPtr - sizeof(int), value.Length, false, false, ref storageApi); + + while (!RespWriteUtils.WriteInteger(initialEtag, ref dcurr, dend)) + SendAndReset(); + + return true; + } + /// /// SETRANGE /// @@ -672,21 +830,30 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt { var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), - ref Unsafe.AsRef(inputPtr), ref o); - + ref Unsafe.AsRef(inputPtr), ref o, cmd); + // Status tells us whether an old image was found during RMW or not - if (status == GarnetStatus.NOTFOUND) - { - Debug.Assert(o.IsSpanByte); - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) - SendAndReset(); - } - else + switch (status) { - if (!o.IsSpanByte) - SendAndReset(o.Memory, o.Length); - else - dcurr += o.Length; + case GarnetStatus.NOTFOUND: + Debug.Assert(o.IsSpanByte); + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.ETAGMISMATCH: + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + default: + if (!o.IsSpanByte) + SendAndReset(o.Memory, o.Length); + else + dcurr += o.Length; + break; } } else @@ -697,16 +864,17 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt bool ok = status != GarnetStatus.NOTFOUND; // Status tells us whether an old image was found during RMW or not - // For a "set if not exists", NOTFOUND means the operation succeeded + // For a "set if not exists" or "set with etag", NOTFOUND means the operation succeeded // So we invert the ok flag - if (cmd == RespCommand.SETEXNX) + if (cmd == RespCommand.SETEXNX || cmd == RespCommand.SETWITHETAG) ok = !ok; if (!ok) { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); } - else + // SETWITHETAG writes back the initial ETAG set back to client outside of this method + else if (cmd != RespCommand.SETWITHETAG) { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index da69aba555..fd47741c65 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -114,6 +114,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_PONG => "+PONG\r\n"u8; public static ReadOnlySpan RESP_EMPTY => "$0\r\n\r\n"u8; public static ReadOnlySpan RESP_QUEUED => "+QUEUED\r\n"u8; + public static ReadOnlySpan RESP_VALNOTCHANGED => "+NOTCHANGED\r\n"u8; /// /// Simple error response strings, i.e. these are of the form "-errorString\r\n" @@ -186,6 +187,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_XX_NX_NOT_COMPATIBLE => "ERR XX and NX options at the same time are not compatible"u8; public static ReadOnlySpan RESP_ERR_GT_LT_NX_NOT_COMPATIBLE => "ERR GT, LT, and/or NX options at the same time are not compatible"u8; public static ReadOnlySpan RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8; + public static ReadOnlySpan RESP_ETAGMISMTACH => "ETAGMISMATCH Given ETag does not match existing ETag."u8; /// /// Response string templates diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index ab36128081..19fb7b2e5e 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -31,7 +31,9 @@ public enum RespCommand : byte GEOSEARCH, GET, GETBIT, + GETIFNOTMATCH, GETRANGE, + GETWITHETAG, HEXISTS, HGET, HGETALL, @@ -130,9 +132,11 @@ public enum RespCommand : byte SETEX, SETEXNX, SETEXXX, + SETIFMATCH, SETKEEPTTL, SETKEEPTTLXX, SETRANGE, + SETWITHETAG, SINTERSTORE, SMOVE, SPOP, @@ -1319,8 +1323,11 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SDIFFSTORE; } + else if (*(ulong*)(ptr + 1) == MemoryMarshal.Read("10\r\nSETI"u8) && *(ulong*)(ptr + 9) == MemoryMarshal.Read("FMATCH\r\n"u8)) + { + return RespCommand.SETIFMATCH; + } break; - case 11: if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nUNSUB"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("SCRIBE\r\n"u8)) { @@ -1346,6 +1353,14 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.SINTERSTORE; } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nGETWI"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("THETAG\r\n"u8)) + { + return RespCommand.GETWITHETAG; + } + else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nSETWI"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("THETAG\r\n"u8)) + { + return RespCommand.SETWITHETAG; + } break; case 12: @@ -1364,6 +1379,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.ZRANGEBYSCORE; } + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("\nGETIFNO"u8) && *(ulong*)(ptr + 12) == MemoryMarshal.Read("TMATCH\r\n"u8)) + { + return RespCommand.GETIFNOTMATCH; + } break; case 14: diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index d611922a88..3212c8bd26 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -506,10 +506,14 @@ private bool ProcessBasicCommands(RespCommand cmd, ref TGarnetApi st _ = cmd switch { RespCommand.GET => NetworkGET(ref storageApi), + RespCommand.GETWITHETAG => NetworkGETWITHETAG(ref storageApi), + RespCommand.GETIFNOTMATCH => NetworkGETIFNOTMATCH(ref storageApi), RespCommand.SET => NetworkSET(ref storageApi), RespCommand.SETEX => NetworkSETEX(false, ref storageApi), RespCommand.PSETEX => NetworkSETEX(true, ref storageApi), RespCommand.SETEXNX => NetworkSETEXNX(ref storageApi), + RespCommand.SETWITHETAG => NetworkSETWITHETAG(ref storageApi), + RespCommand.SETIFMATCH => NetworkSETIFMATCH(ref storageApi), RespCommand.DEL => NetworkDEL(ref storageApi), RespCommand.RENAME => NetworkRENAME(ref storageApi), RespCommand.RENAMENX => NetworkRENAMENX(ref storageApi), diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index c24563ef50..e66d9941f5 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -36,7 +36,7 @@ static void CopyTo(ref SpanByte src, ref SpanByteAndMemory dst, MemoryPool void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int end = -1) { - int srcLength = end == -1 ? src.LengthWithoutMetadata : ((start < end) ? (end - start) : 0); + int srcLength = end == -1 ? src.LengthWithoutMetadata : Math.Max(end - start, 0); if (srcLength == 0) { CopyDefaultResp(CmdStrings.RESP_EMPTY, ref dst); @@ -82,7 +82,7 @@ void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int } } - void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending) + void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending, int payloadEtagEnd, int etagIgnoredDataEnd) { var inputPtr = input.ToPointer(); switch ((RespCommand)(*inputPtr)) @@ -93,7 +93,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd // This is accomplished by calling ConvertToHeap on the destination SpanByteAndMemory if (isFromPending) dst.ConvertToHeap(); - CopyRespTo(ref value, ref dst); + CopyRespTo(ref value, ref dst, payloadEtagEnd, etagIgnoredDataEnd); break; case RespCommand.MIGRATE: @@ -124,18 +124,18 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd if (value.LengthWithoutMetadata <= dst.Length) { dst.Length = value.LengthWithoutMetadata; - value.AsReadOnlySpan().CopyTo(dst.SpanByte.AsSpan()); + value.AsReadOnlySpan(payloadEtagEnd).CopyTo(dst.SpanByte.AsSpan()); return; } dst.ConvertToHeap(); dst.Length = value.LengthWithoutMetadata; dst.Memory = functionsState.memoryPool.Rent(value.LengthWithoutMetadata); - value.AsReadOnlySpan().CopyTo(dst.Memory.Memory.Span); + value.AsReadOnlySpan(payloadEtagEnd).CopyTo(dst.Memory.Memory.Span); break; case RespCommand.GETBIT: - byte oldValSet = BitmapManager.GetBit(inputPtr + RespInputHeader.Size, value.ToPointer(), value.Length); + byte oldValSet = BitmapManager.GetBit(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); if (oldValSet == 0) CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_0, ref dst); else @@ -143,18 +143,18 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd break; case RespCommand.BITCOUNT: - long count = BitmapManager.BitCountDriver(inputPtr + RespInputHeader.Size, value.ToPointer(), value.Length); + long count = BitmapManager.BitCountDriver(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); CopyRespNumber(count, ref dst); break; case RespCommand.BITPOS: - long pos = BitmapManager.BitPosDriver(inputPtr + RespInputHeader.Size, value.ToPointer(), value.Length); + long pos = BitmapManager.BitPosDriver(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); *(long*)dst.SpanByte.ToPointer() = pos; CopyRespNumber(pos, ref dst); break; case RespCommand.BITOP: - IntPtr bitmap = (IntPtr)value.ToPointer(); + IntPtr bitmap = (IntPtr)value.ToPointer() + payloadEtagEnd; byte* output = dst.SpanByte.ToPointer(); *(long*)output = (long)bitmap.ToInt64(); @@ -165,7 +165,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd case RespCommand.BITFIELD: long retValue = 0; bool overflow; - (retValue, overflow) = BitmapManager.BitFieldExecute(inputPtr + RespInputHeader.Size, value.ToPointer(), value.Length); + (retValue, overflow) = BitmapManager.BitFieldExecute(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); if (!overflow) CopyRespNumber(retValue, ref dst); else @@ -173,28 +173,28 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd return; case RespCommand.PFCOUNT: - if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer(), value.Length)) + if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd)) { *(long*)dst.SpanByte.ToPointer() = -1; return; } long E = 13; - E = HyperLogLog.DefaultHLL.Count(value.ToPointer()); + E = HyperLogLog.DefaultHLL.Count(value.ToPointer() + payloadEtagEnd); *(long*)dst.SpanByte.ToPointer() = E; return; case RespCommand.PFMERGE: - if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer(), value.Length)) + if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd)) { *(long*)dst.SpanByte.ToPointer() = -1; return; } - if (value.Length <= dst.Length) + if (value.Length - payloadEtagEnd <= dst.Length) { - Buffer.MemoryCopy(value.ToPointer(), dst.SpanByte.ToPointer(), value.Length, value.Length); - dst.SpanByte.Length = value.Length; + Buffer.MemoryCopy(value.ToPointer() + payloadEtagEnd, dst.SpanByte.ToPointer(), value.Length - payloadEtagEnd, value.Length - payloadEtagEnd); + dst.SpanByte.Length = value.Length - payloadEtagEnd; return; } throw new GarnetException("Not enough space in PFMERGE buffer"); @@ -210,18 +210,63 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd return; case RespCommand.GETRANGE: - int len = value.LengthWithoutMetadata; + int len = value.LengthWithoutMetadata - payloadEtagEnd; int start = *(int*)(inputPtr + RespInputHeader.Size); int end = *(int*)(inputPtr + RespInputHeader.Size + 4); (start, end) = NormalizeRange(start, end, len); - CopyRespTo(ref value, ref dst, start, end); + CopyRespTo(ref value, ref dst, start + payloadEtagEnd, end + payloadEtagEnd); + return; + case RespCommand.GETIFNOTMATCH: + case RespCommand.SETIFMATCH: + case RespCommand.GETWITHETAG: + // Get value without RESP header; exclude expiration + // extract ETAG, write as long into dst, and then value + long etag = *(long*)value.ToPointer(); + // remove the length of the ETAG + int valueLength = value.LengthWithoutMetadata - sizeof(long); + // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below + ReadOnlySpan etagTruncatedVal = value.AsReadOnlySpan(sizeof(long)); + // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n + int desiredLength = 4 + 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; + WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); return; default: throw new GarnetException("Unsupported operation on input"); } } + void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst) + { + if (desiredLength <= dst.Length) + { + dst.Length = desiredLength; + byte* curr = dst.SpanByte.ToPointer(); + byte* end = curr + dst.SpanByte.Length; + RespWriteEtagValArray(etag, ref value, ref curr, end); + return; + } + + dst.Length = desiredLength; + dst.ConvertToHeap(); + dst.Memory = functionsState.memoryPool.Rent(desiredLength); + fixed (byte* ptr = dst.Memory.Memory.Span) + { + byte* curr = ptr; + byte* end = ptr + desiredLength; + RespWriteEtagValArray(etag, ref value, ref curr, end); + } + } + + static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end) + { + // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element + var initPtr = curr; + RespWriteUtils.WriteArrayLength(2, ref curr, end); + RespWriteUtils.WriteInteger(etag, ref curr, end); + RespWriteUtils.WriteBulkString(value, ref curr, end); + } + bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory output) { ObjectOutputHeader* o = (ObjectOutputHeader*)output.SpanByte.ToPointer(); @@ -276,7 +321,7 @@ bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, ref SpanB } } - void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref SpanByte input, ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output) + void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref SpanByte input, ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output, int etagIgnoredOffset) { ObjectOutputHeader* o = (ObjectOutputHeader*)output.SpanByte.ToPointer(); if (expiryExists) @@ -284,16 +329,16 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp switch (optionType) { case ExpireOption.NX: - oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); + oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); break; case ExpireOption.XX: case ExpireOption.None: newValue.ExtraMetadata = input.ExtraMetadata; - oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); + oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); o->result1 = 1; break; case ExpireOption.GT: - oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); + oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); bool replace = input.ExtraMetadata < oldValue.ExtraMetadata; newValue.ExtraMetadata = replace ? oldValue.ExtraMetadata : input.ExtraMetadata; if (replace) @@ -302,7 +347,7 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp o->result1 = 1; break; case ExpireOption.LT: - oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); + oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); replace = input.ExtraMetadata > oldValue.ExtraMetadata; newValue.ExtraMetadata = replace ? oldValue.ExtraMetadata : input.ExtraMetadata; if (replace) @@ -319,13 +364,13 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp case ExpireOption.NX: case ExpireOption.None: newValue.ExtraMetadata = input.ExtraMetadata; - oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); + oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); o->result1 = 1; break; case ExpireOption.XX: case ExpireOption.GT: case ExpireOption.LT: - oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); + oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); o->result1 = 0; break; } @@ -357,30 +402,31 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp internal static bool CheckExpiry(ref SpanByte src) => src.ExtraMetadata < DateTimeOffset.UtcNow.Ticks; - static bool InPlaceUpdateNumber(long val, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo) + static bool InPlaceUpdateNumber(long val, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, int valueOffset) { var fNeg = false; var ndigits = NumUtils.NumDigitsInLong(val, ref fNeg); ndigits += fNeg ? 1 : 0; - if (ndigits > value.LengthWithoutMetadata) + if (ndigits > value.LengthWithoutMetadata - valueOffset) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - value.ShrinkSerializedLength(ndigits + value.MetadataSize); - _ = NumUtils.LongToSpanByte(val, value.AsSpan()); + value.ShrinkSerializedLength(ndigits + value.MetadataSize + valueOffset); + _ = NumUtils.LongToSpanByte(val, value.AsSpan(valueOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); Debug.Assert(output.IsSpanByte, "This code assumes it is called in-place and did not go pending"); - value.AsReadOnlySpan().CopyTo(output.SpanByte.AsSpan()); - output.SpanByte.Length = value.LengthWithoutMetadata; + value.AsReadOnlySpan(valueOffset).CopyTo(output.SpanByte.AsSpan()); + output.SpanByte.Length = value.LengthWithoutMetadata - valueOffset; return true; } - static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, long input) + static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, long input, int valueOffset) { + var debuggerCheck = value.ToPointer(); // Check if value contains a valid number - if (!IsValidNumber(value.LengthWithoutMetadata, value.ToPointer(), output.SpanByte.AsSpan(), out var val)) + if (!IsValidNumber(value.LengthWithoutMetadata - valueOffset, value.ToPointer() + valueOffset, output.SpanByte.AsSpan(), out var val)) return true; try @@ -393,14 +439,14 @@ static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory out return true; } - return InPlaceUpdateNumber(val, ref value, ref output, ref rmwInfo, ref recordInfo); + return InPlaceUpdateNumber(val, ref value, ref output, ref rmwInfo, ref recordInfo, valueOffset); } - static void CopyUpdateNumber(long next, ref SpanByte newValue, ref SpanByteAndMemory output) + static void CopyUpdateNumber(long next, ref SpanByte newValue, ref SpanByteAndMemory output, int etagIgnoredOffset) { - NumUtils.LongToSpanByte(next, newValue.AsSpan()); - newValue.AsReadOnlySpan().CopyTo(output.SpanByte.AsSpan()); - output.SpanByte.Length = newValue.LengthWithoutMetadata; + NumUtils.LongToSpanByte(next, newValue.AsSpan(etagIgnoredOffset)); + newValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(output.SpanByte.AsSpan()); + output.SpanByte.Length = newValue.LengthWithoutMetadata - etagIgnoredOffset; } /// @@ -410,12 +456,12 @@ static void CopyUpdateNumber(long next, ref SpanByte newValue, ref SpanByteAndMe /// New value copying to /// Output value /// Parsed input value - static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output, long input) + static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output, long input, int etagIgnoredOffset) { newValue.ExtraMetadata = oldValue.ExtraMetadata; // Check if value contains a valid number - if (!IsValidNumber(oldValue.LengthWithoutMetadata, oldValue.ToPointer(), output.SpanByte.AsSpan(), out var val)) + if (!IsValidNumber(oldValue.LengthWithoutMetadata - etagIgnoredOffset, oldValue.ToPointer() + etagIgnoredOffset, output.SpanByte.AsSpan(), out var val)) { // Move to tail of the log even when oldValue is alphanumeric // We have already paid the cost of bringing from disk so we are treating as a regular access and bring it into memory @@ -435,7 +481,7 @@ static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, re } // Move to tail of the log and update - CopyUpdateNumber(val, ref newValue, ref output); + CopyUpdateNumber(val, ref newValue, ref output, etagIgnoredOffset); } /// @@ -510,13 +556,13 @@ void CopyRespNumber(long number, ref SpanByteAndMemory dst) /// /// Copy length of value to output (as ASCII bytes) /// - static void CopyValueLengthToOutput(ref SpanByte value, ref SpanByteAndMemory output) + static void CopyValueLengthToOutput(ref SpanByte value, ref SpanByteAndMemory output, int adjustForEtagLen) { - int numDigits = NumUtils.NumDigits(value.LengthWithoutMetadata); + int numDigits = NumUtils.NumDigits(value.LengthWithoutMetadata - adjustForEtagLen); Debug.Assert(output.IsSpanByte, "This code assumes it is called in a non-pending context or in a pending context where dst.SpanByte's pointer remains valid"); var outputPtr = output.SpanByte.ToPointer(); - NumUtils.IntToBytes(value.LengthWithoutMetadata, numDigits, ref outputPtr); + NumUtils.IntToBytes(value.LengthWithoutMetadata - adjustForEtagLen, numDigits, ref outputPtr); output.SpanByte.Length = numDigits; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index f05a01c617..1e8b4cb0f5 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -20,6 +20,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte var cmd = input.AsSpan()[0]; switch ((RespCommand)cmd) { + case RespCommand.SETIFMATCH: case RespCommand.SETKEEPTTLXX: case RespCommand.SETEXXX: case RespCommand.PERSIST: @@ -121,7 +122,7 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va var newValuePtr = new Span((byte*)*(long*)(inputPtr + RespInputHeader.Size + sizeof(int) * 2), newValueSize); newValuePtr.CopyTo(value.AsSpan().Slice(offset)); - CopyValueLengthToOutput(ref value, ref output); + CopyValueLengthToOutput(ref value, ref output, 0); break; case RespCommand.APPEND: @@ -130,8 +131,8 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va var appendPtr = *(long*)(inputPtr + RespInputHeader.Size + sizeof(int)); var appendSpan = new Span((byte*)appendPtr, appendSize); appendSpan.CopyTo(value.AsSpan()); - - CopyValueLengthToOutput(ref value, ref output); + // HK TODO: Test + CopyValueLengthToOutput(ref value, ref output, 0); break; case RespCommand.INCRBY: value.UnmarkExtraMetadata(); @@ -139,7 +140,8 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va length = input.LengthWithoutMetadata - RespInputHeader.Size; if (!IsValidNumber(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out var incrBy)) return false; - CopyUpdateNumber(incrBy, ref value, ref output); + // If incrby is being made for initial update then it was not made with etag so the offset is sent as 0 + CopyUpdateNumber(incrBy, ref value, ref output, 0); break; case RespCommand.DECRBY: value.UnmarkExtraMetadata(); @@ -147,7 +149,8 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va length = input.LengthWithoutMetadata - RespInputHeader.Size; if (!IsValidNumber(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out var decrBy)) return false; - CopyUpdateNumber(-decrBy, ref value, ref output); + // If incrby is being made for initial update then it was not made with etag so the offset is sent as 0 + CopyUpdateNumber(-decrBy, ref value, ref output, 0); break; default: value.UnmarkExtraMetadata(); @@ -175,6 +178,9 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va break; } + if ((RespCommand)(*inputPtr) == RespCommand.SETWITHETAG) + recordInfo.SetHasETag(); + // Copy input to value value.ShrinkSerializedLength(input.Length - RespInputHeader.Size); value.ExtraMetadata = input.ExtraMetadata; @@ -227,40 +233,88 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span } // First byte of input payload identifies command - switch ((RespCommand)(*inputPtr)) + var cmd = (RespCommand)(*inputPtr); + + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = -1; + if (recordInfo.ETag) + { + etagIgnoredOffset = sizeof(long); + etagIgnoredEnd = value.LengthWithoutMetadata; + oldEtag = *(long*)value.ToPointer(); + } + + switch (cmd) { + // HK TODO figure out where this logic is? case RespCommand.SETEXNX: // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + } + break; + + case RespCommand.SETIFMATCH: + if (!recordInfo.ETag) + { + return false; + } + + long prevEtag = *(long*)value.ToPointer(); + long etagFromClient = *(long*)(inputPtr + RespInputHeader.Size); + if (prevEtag != etagFromClient) + { + // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH + rmwInfo.Action = RMWAction.CancelOperation; + return false; } + + // Need CU if no space for new value + if (input.Length - RespInputHeader.Size > value.Length) return false; + + // Increment the ETag + *(long*)(input.ToPointer() + RespInputHeader.Size) += 1; + + // Adjust value length + rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); + value.UnmarkExtraMetadata(); + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size); + + // Copy input to value + value.ExtraMetadata = input.ExtraMetadata; + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan()); + rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + + CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1); + // early return since we already updated the ETag return true; case RespCommand.SET: case RespCommand.SETEXXX: // Need CU if no space for new value - if (input.Length - RespInputHeader.Size > value.Length) return false; + if (input.Length - RespInputHeader.Size > value.Length - etagIgnoredOffset) return false; // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size); + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + etagIgnoredOffset); // Copy input to value value.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan()); + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - return true; + break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: @@ -271,7 +325,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Adjust value length @@ -279,9 +333,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span value.ShrinkSerializedLength(value.MetadataSize + input.Length - RespInputHeader.Size); // Copy input to value - input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(value.AsSpan()); + input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - return true; + break; case RespCommand.PEXPIRE: case RespCommand.EXPIRE: @@ -290,6 +344,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span return EvaluateExpireInPlace(optionType, expiryExists, ref input, ref value, ref output); case RespCommand.PERSIST: + // HK TODO: NO clue about this one if (value.MetadataSize != 0) { rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); @@ -299,33 +354,41 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); output.SpanByte.AsSpan()[0] = 1; } - return true; + break; case RespCommand.INCR: - return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1); - + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) + return false; + break; case RespCommand.DECR: - return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1); + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, etagIgnoredOffset)) + return false; + break; case RespCommand.INCRBY: var length = input.LengthWithoutMetadata - RespInputHeader.Size; // Check if input contains a valid number if (!IsValidNumber(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out var incrBy)) return true; - return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy); + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, etagIgnoredOffset)) + return false; + break; case RespCommand.DECRBY: length = input.LengthWithoutMetadata - RespInputHeader.Size; // Check if input contains a valid number if (!IsValidNumber(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out var decrBy)) return true; - return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy); + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, etagIgnoredOffset)) + return false; + break; case RespCommand.SETBIT: byte* i = inputPtr + RespInputHeader.Size; - byte* v = value.ToPointer(); + byte* v = value.ToPointer() + etagIgnoredOffset; - if (!BitmapManager.IsLargeEnough(i, value.Length)) return false; + // the "- etagIgnoredOffset" accounts for subtracting the space for the etag in the payload if it exists in the Value + if (!BitmapManager.IsLargeEnough(i, value.Length - etagIgnoredOffset)) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); @@ -340,8 +403,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span return true; case RespCommand.BITFIELD: i = inputPtr + RespInputHeader.Size; - v = value.ToPointer(); - if (!BitmapManager.IsLargeEnoughForType(i, value.Length)) return false; + v = value.ToPointer() + etagIgnoredOffset; + + // the "- etagIgnoredOffset" accounts for subtracting the space for the etag in the payload if it exists in the Value + if (!BitmapManager.IsLargeEnoughForType(i, value.Length - etagIgnoredOffset)) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); @@ -356,11 +421,12 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span CopyRespNumber(bitfieldReturnValue, ref output); else CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); - return true; + + break; case RespCommand.PFADD: i = inputPtr + RespInputHeader.Size; - v = value.ToPointer(); + v = value.ToPointer() + etagIgnoredOffset; if (!HyperLogLog.DefaultHLL.IsValidHYLL(v, value.Length)) { @@ -374,14 +440,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span var result = HyperLogLog.DefaultHLL.Update(i, v, value.Length, ref updated); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (result) - *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; - return result; + if (!result) + return false; + + *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; + break; case RespCommand.PFMERGE: //srcHLL offset: [hll allocated size = 4 byte] + [hll data structure] //memcpy +4 (skip len size) byte* srcHLL = inputPtr + RespInputHeader.Size + sizeof(int); - byte* dstHLL = value.ToPointer(); + byte* dstHLL = value.ToPointer() + etagIgnoredOffset; if (!HyperLogLog.DefaultHLL.IsValidHYLL(dstHLL, value.Length)) { @@ -392,7 +460,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(value.Length + value.MetadataSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - return HyperLogLog.DefaultHLL.TryMerge(srcHLL, dstHLL, value.Length); + if (!HyperLogLog.DefaultHLL.TryMerge(srcHLL, dstHLL, value.Length)) + return false; + + break; + case RespCommand.SETRANGE: var offset = *(int*)(inputPtr + RespInputHeader.Size); var newValueSize = *(int*)(inputPtr + RespInputHeader.Size + sizeof(int)); @@ -401,15 +473,15 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span if (newValueSize + offset > value.LengthWithoutMetadata) return false; - newValuePtr.CopyTo(value.AsSpan().Slice(offset)); + newValuePtr.CopyTo(value.AsSpan(etagIgnoredOffset).Slice(offset)); - CopyValueLengthToOutput(ref value, ref output); + CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); return true; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref value, ref output); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); rmwInfo.Action = RMWAction.ExpireAndStop; return false; @@ -420,8 +492,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span if (appendSize == 0) { - CopyValueLengthToOutput(ref value, ref output); - return true; + CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); + break; } return false; @@ -453,13 +525,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span value.ExtraMetadata = expiration; } - int valueLength = value.LengthWithoutMetadata; + int valueLength = value.LengthWithoutMetadata - etagIgnoredOffset; (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); - var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), input.AsReadOnlySpan()[RespInputHeader.Size..], value.AsSpan(), ref valueLength, ref outp, ref rmwInfo); + var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), input.AsReadOnlySpan()[RespInputHeader.Size..], value.AsSpan(etagIgnoredOffset), ref valueLength, ref outp, ref rmwInfo); Debug.Assert(valueLength <= value.LengthWithoutMetadata); // Adjust value length if user shrinks it - if (valueLength < value.LengthWithoutMetadata) + if (valueLength < value.LengthWithoutMetadata - etagIgnoredOffset) { rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(valueLength + value.MetadataSize); @@ -468,18 +540,52 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span output.Memory = outp.Memory; output.Length = outp.Length; - return ret; + if (!ret) + return false; + + break; } throw new GarnetException("Unsupported operation on input"); } + + // increment the Etag transparently if in place update happened + if (recordInfo.ETag && rmwInfo.Action == RMWAction.Default) + { + *(long*)value.ToPointer() = oldEtag + 1; + } + return true; } /// public bool NeedCopyUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { var inputPtr = input.ToPointer(); + + var cmd = (RespCommand)(*inputPtr); + + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + if (rmwInfo.RecordInfo.ETag) + { + etagIgnoredOffset = sizeof(long); + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + } + switch ((RespCommand)(*inputPtr)) { + case RespCommand.SETIFMATCH: + if (!rmwInfo.RecordInfo.ETag) + return false; + + long existingEtag = *(long*)oldValue.ToPointer(); + long etagToCheckWith = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag != etagToCheckWith) + { + // cancellation and return false indicates ETag mismatch + rmwInfo.Action = RMWAction.CancelOperation; + return false; + } + return true; case RespCommand.SETEXNX: // Expired data, return false immediately // ExpireAndStop ensures that caller sees a NOTFOUND status @@ -492,14 +598,14 @@ public bool NeedCopyUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte ol if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } return false; default: if (*inputPtr >= CustomCommandManager.StartOffset) { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); - var ret = functionsState.customCommands[*inputPtr - CustomCommandManager.StartOffset].functions.NeedCopyUpdate(key.AsReadOnlySpan(), input.AsReadOnlySpan()[RespInputHeader.Size..], oldValue.AsReadOnlySpan(), ref outp); + var ret = functionsState.customCommands[*inputPtr - CustomCommandManager.StartOffset].functions.NeedCopyUpdate(key.AsReadOnlySpan(), input.AsReadOnlySpan()[RespInputHeader.Size..], oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -523,8 +629,33 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - switch ((RespCommand)(*inputPtr)) + var cmd = (RespCommand)(*inputPtr); + + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = -1; + if (recordInfo.ETag) + { + etagIgnoredOffset = sizeof(long); + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + oldEtag = *(long*)oldValue.ToPointer(); + } + + switch (cmd) { + case RespCommand.SETIFMATCH: + Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data."); + + // update the etag + *(long*)(input.ToPointer() + RespInputHeader.Size) += 1; + // Copy input to value + newValue.ExtraMetadata = input.ExtraMetadata; + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan()); + + // Write Etag and Val back to Client + CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1); + break; + case RespCommand.SET: case RespCommand.SETEXXX: Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length); @@ -533,12 +664,13 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan()); + // HK TODO: SETEXXX for a SETWITHETAG value says newvalue does not have enoug bytes here + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(etagIgnoredOffset)); break; case RespCommand.SETKEEPTTLXX: @@ -549,12 +681,12 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Copy input to value, retain metadata of oldValue newValue.ExtraMetadata = oldValue.ExtraMetadata; - input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(newValue.AsSpan()); + input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(newValue.AsSpan(etagIgnoredOffset)); break; case RespCommand.EXPIRE: @@ -562,7 +694,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa Debug.Assert(newValue.Length == oldValue.Length + input.MetadataSize); ExpireOption optionType = (ExpireOption)(*(inputPtr + RespInputHeader.Size)); bool expiryExists = oldValue.MetadataSize > 0; - EvaluateExpireCopyUpdate(optionType, expiryExists, ref input, ref oldValue, ref newValue, ref output); + EvaluateExpireCopyUpdate(optionType, expiryExists, ref input, ref oldValue, ref newValue, ref output, etagIgnoredOffset); break; case RespCommand.PERSIST: @@ -577,11 +709,11 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa break; case RespCommand.INCR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1, etagIgnoredOffset); break; case RespCommand.DECR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1, etagIgnoredOffset); break; case RespCommand.INCRBY: @@ -593,7 +725,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa oldValue.CopyTo(ref newValue); break; } - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy, etagIgnoredOffset); break; case RespCommand.DECRBY: @@ -605,11 +737,11 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa oldValue.CopyTo(ref newValue); break; } - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy, etagIgnoredOffset); break; case RespCommand.SETBIT: - Buffer.MemoryCopy(oldValue.ToPointer(), newValue.ToPointer(), newValue.Length, oldValue.Length); + Buffer.MemoryCopy(oldValue.ToPointer() + etagIgnoredOffset, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); byte oldValSet = BitmapManager.UpdateBitmap(inputPtr + RespInputHeader.Size, newValue.ToPointer()); if (oldValSet == 0) CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_0, ref output); @@ -618,10 +750,10 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa break; case RespCommand.BITFIELD: - Buffer.MemoryCopy(oldValue.ToPointer(), newValue.ToPointer(), newValue.Length, oldValue.Length); + Buffer.MemoryCopy(oldValue.ToPointer() + etagIgnoredOffset, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); long bitfieldReturnValue; bool overflow; - (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(inputPtr + RespInputHeader.Size, newValue.ToPointer(), newValue.Length); + (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(inputPtr + RespInputHeader.Size, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset); if (!overflow) CopyRespNumber(bitfieldReturnValue, ref output); @@ -631,15 +763,15 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.PFADD: bool updated = false; - byte* newValPtr = newValue.ToPointer(); - byte* oldValPtr = oldValue.ToPointer(); + byte* newValPtr = newValue.ToPointer() + etagIgnoredOffset; + byte* oldValPtr = oldValue.ToPointer() + etagIgnoredOffset; if (newValue.Length != oldValue.Length) - updated = HyperLogLog.DefaultHLL.CopyUpdate(inputPtr + RespInputHeader.Size, oldValPtr, newValPtr, newValue.Length); + updated = HyperLogLog.DefaultHLL.CopyUpdate(inputPtr + RespInputHeader.Size, oldValPtr, newValPtr, newValue.Length - etagIgnoredOffset); else { - Buffer.MemoryCopy(oldValPtr, newValPtr, newValue.Length, oldValue.Length); - HyperLogLog.DefaultHLL.Update(inputPtr + RespInputHeader.Size, newValPtr, newValue.Length, ref updated); + Buffer.MemoryCopy(oldValPtr, newValPtr, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); + HyperLogLog.DefaultHLL.Update(inputPtr + RespInputHeader.Size, newValPtr, newValue.Length - etagIgnoredOffset, ref updated); } *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; break; @@ -647,10 +779,10 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.PFMERGE: //srcA offset: [hll allocated size = 4 byte] + [hll data structure] //memcpy +4 (skip len size) byte* srcHLLPtr = inputPtr + RespInputHeader.Size + sizeof(int); // HLL merging from - byte* oldDstHLLPtr = oldValue.ToPointer(); // original HLL merging to (too small to hold its data plus srcA) - byte* newDstHLLPtr = newValue.ToPointer(); // new HLL merging to (large enough to hold srcA and srcB + byte* oldDstHLLPtr = oldValue.ToPointer() + etagIgnoredOffset; // original HLL merging to (too small to hold its data plus srcA) + byte* newDstHLLPtr = newValue.ToPointer() + etagIgnoredOffset; // new HLL merging to (large enough to hold srcA and srcB - HyperLogLog.DefaultHLL.CopyUpdateMerge(srcHLLPtr, oldDstHLLPtr, newDstHLLPtr, oldValue.Length, newValue.Length); + HyperLogLog.DefaultHLL.CopyUpdateMerge(srcHLLPtr, oldDstHLLPtr, newDstHLLPtr, oldValue.Length -etagIgnoredOffset, newValue.Length - etagIgnoredOffset); break; case RespCommand.SETRANGE: @@ -659,9 +791,9 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa var newValueSize = *(int*)(inputPtr + RespInputHeader.Size + sizeof(int)); var newValuePtr = new Span((byte*)*(long*)(inputPtr + RespInputHeader.Size + sizeof(int) * 2), newValueSize); - newValuePtr.CopyTo(newValue.AsSpan().Slice(offset)); + newValuePtr.CopyTo(newValue.AsSpan(etagIgnoredOffset).Slice(offset)); - CopyValueLengthToOutput(ref newValue, ref output); + CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); break; case RespCommand.GETDEL: @@ -680,9 +812,9 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa var appendSpan = new Span((byte*)appendPtr, appendSize); // Append the new value with the client input at the end of the old data - appendSpan.CopyTo(newValue.AsSpan().Slice(oldValue.LengthWithoutMetadata)); + appendSpan.CopyTo(newValue.AsSpan(etagIgnoredOffset).Slice(oldValue.LengthWithoutMetadata)); - CopyValueLengthToOutput(ref newValue, ref output); + CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); break; default: @@ -704,7 +836,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.customCommands[*inputPtr - CustomCommandManager.StartOffset].functions.CopyUpdater(key.AsReadOnlySpan(), input.AsReadOnlySpan().Slice(RespInputHeader.Size), - oldValue.AsReadOnlySpan(), newValue.AsSpan(), ref outp, ref rmwInfo); + oldValue.AsReadOnlySpan(etagIgnoredOffset), newValue.AsSpan(etagIgnoredOffset), ref outp, ref rmwInfo); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -713,6 +845,13 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa } rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); + + // increment the Etag transparently if in place update happened + if (recordInfo.ETag) + { + *(long*)newValue.ToPointer() = oldEtag + 1; + } + return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 36b74eb0f1..a67093828e 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -19,6 +19,17 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu return false; var cmd = ((RespInputHeader*)input.ToPointer())->cmd; + + var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; + + // ETAG Read command on non-ETag data should early exit but indicate the wrong type + if (isEtagCmd && !readInfo.RecordInfo.ETag) + { + // Used to indicate wrong type operation + readInfo.Action = ReadAction.CancelOperation; + return false; + } + if ((byte)cmd >= CustomCommandManager.StartOffset) { int valueLength = value.LengthWithoutMetadata; @@ -30,10 +41,31 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu return ret; } + if (cmd == RespCommand.GETIFNOTMATCH) + { + long existingEtag = *(long*)value.ToPointer(); + long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag == etagToMatchAgainst) + { + // write the value not changed message to dst, and early return + CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + return true; + } + } + + // Unless the command explicitly asks for the ETag in response, we do not write back the ETag + var start = 0; + var end = -1; + if (!isEtagCmd && readInfo.RecordInfo.ETag) + { + start = sizeof(long); + end = value.LengthWithoutMetadata; + } + if (input.Length == 0) - CopyRespTo(ref value, ref dst); + CopyRespTo(ref value, ref dst, start, end); else - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end); return true; } @@ -49,6 +81,17 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte } var cmd = ((RespInputHeader*)input.ToPointer())->cmd; + + var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; + + // ETAG Read command on non-ETag data should early exit but indicate the wrong type + if (isEtagCmd && !recordInfo.ETag) + { + // Used to indicate wrong type operation + readInfo.Action = ReadAction.CancelOperation; + return false; + } + if ((byte)cmd >= CustomCommandManager.StartOffset) { int valueLength = value.LengthWithoutMetadata; @@ -60,11 +103,32 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte return ret; } + if (cmd == RespCommand.GETIFNOTMATCH) + { + long existingEtag = *(long*)value.ToPointer(); + long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag == etagToMatchAgainst) + { + // write the value not changed message to dst, and early return + CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + return true; + } + } + + // Unless the command explicitly asks for the ETag in response, we do not write back the ETag + var start = 0; + var end = -1; + if (!isEtagCmd && recordInfo.ETag) + { + start = sizeof(long); + end = value.LengthWithoutMetadata; + } + if (input.Length == 0) - CopyRespTo(ref value, ref dst); + CopyRespTo(ref value, ref dst, start, end); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end); } return true; diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 6c9d812d87..a608158ff0 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -164,6 +164,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) case RespCommand.SETKEEPTTL: return sizeof(int) + t.MetadataSize + input.Length - RespInputHeader.Size; + case RespCommand.SETIFMATCH: case RespCommand.SET: case RespCommand.SETEXXX: case RespCommand.PERSIST: diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index eed91fe0a0..ee6a65b2a0 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; using Garnet.common; @@ -42,6 +43,40 @@ public GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref Span } } + // We separate this function altogether for being able to filter out WONGTYPE so no overhead is incurred in the common path from branching + // that this method call would have added in the instructions. This let's GET method calls function without overhead as they did before ETags + // were added + public GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, ref TContext context) + where TContext : ITsavoriteContext + { + long ctx = default; + var status = context.Read(ref key, ref input, ref output, ctx); + + if (status.IsPending) + { + StartPendingMetrics(); + CompletePendingForSession(ref status, ref output, ref context); + StopPendingMetrics(); + } + + if (status.IsCanceled) + { + // Cancelled Read operation on a Get call for any ETag based APIs inidcates that the cmd was applied to a non-etag record + return GarnetStatus.WRONGTYPE; + } + + if (status.Found) + { + incr_session_found(); + return GarnetStatus.OK; + } + else + { + incr_session_notfound(); + return GarnetStatus.NOTFOUND; + } + } + public unsafe GarnetStatus ReadWithUnsafeContext(ArgSlice key, ref SpanByte input, ref SpanByteAndMemory output, long localHeadAddress, out bool epochChanged, ref TContext context) where TContext : ITsavoriteContext, IUnsafeContext { @@ -360,6 +395,36 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref SpanB } } + public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, ref TContext context, RespCommand cmd) + where TContext : ITsavoriteContext + { + var status = context.RMW(ref key, ref input, ref output); + + if (status.IsPending) + { + StartPendingMetrics(); + CompletePendingForSession(ref status, ref output, ref context); + StopPendingMetrics(); + } + + if (status.NotFound) + { + incr_session_notfound(); + return GarnetStatus.NOTFOUND; + } + else if (cmd == RespCommand.SETIFMATCH && !status.IsUpdated) + { + // The RMW operation for SETIFMATCH upon not finding the etags match between the existing record and sent etag returns Cancelled Operation + incr_session_found(); + return status.IsCanceled ? GarnetStatus.ETAGMISMATCH : GarnetStatus.WRONGTYPE; + } + else + { + incr_session_found(); + return GarnetStatus.OK; + } + } + public GarnetStatus SET(ArgSlice key, ArgSlice value, ref TContext context) where TContext : ITsavoriteContext { diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs index b04e793e8a..a98ef6d913 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs @@ -3,7 +3,6 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -12,7 +11,7 @@ namespace Tsavorite.core { // RecordInfo layout (64 bits total): - // [Unused1][Modified][InNewVersion][Filler][Dirty][Unused2][Sealed][Valid][Tombstone][LLLLLLL] [RAAAAAAA] [AAAAAAAA] [AAAAAAAA] [AAAAAAAA] [AAAAAAAA] [AAAAAAAA] + // [Unused1][Modified][InNewVersion][Filler][Dirty][ETag][Sealed][Valid][Tombstone][LLLLLLL] [RAAAAAAA] [AAAAAAAA] [AAAAAAAA] [AAAAAAAA] [AAAAAAAA] [AAAAAAAA] // where L = leftover, R = readcache, A = address [StructLayout(LayoutKind.Explicit, Size = 8)] public struct RecordInfo @@ -31,8 +30,8 @@ public struct RecordInfo const int kTombstoneBitOffset = kPreviousAddressBits + kLeftoverBitCount; const int kValidBitOffset = kTombstoneBitOffset + 1; const int kSealedBitOffset = kValidBitOffset + 1; - const int kUnused2BitOffset = kSealedBitOffset + 1; - const int kDirtyBitOffset = kUnused2BitOffset + 1; + const int kEtagBitOffset = kSealedBitOffset + 1; + const int kDirtyBitOffset = kEtagBitOffset + 1; const int kFillerBitOffset = kDirtyBitOffset + 1; const int kInNewVersionBitOffset = kFillerBitOffset + 1; const int kModifiedBitOffset = kInNewVersionBitOffset + 1; @@ -41,7 +40,7 @@ public struct RecordInfo const long kTombstoneBitMask = 1L << kTombstoneBitOffset; const long kValidBitMask = 1L << kValidBitOffset; const long kSealedBitMask = 1L << kSealedBitOffset; - const long kUnused2BitMask = 1L << kUnused2BitOffset; + const long kETagBitMask = 1L << kEtagBitOffset; const long kDirtyBitMask = 1L << kDirtyBitOffset; const long kFillerBitMask = 1L << kFillerBitOffset; const long kInNewVersionBitMask = 1L << kInNewVersionBitOffset; @@ -277,18 +276,21 @@ internal bool Unused1 set => word = value ? word | kUnused1BitMask : word & ~kUnused1BitMask; } - internal bool Unused2 + public bool ETag { - readonly get => (word & kUnused2BitMask) != 0; - set => word = value ? word | kUnused2BitMask : word & ~kUnused2BitMask; + readonly get => (word & kETagBitMask) != 0; + set => word = value ? word | kETagBitMask : word & ~kETagBitMask; } + public void SetHasETag() => word |= kETagBitMask; + internal void ClearHasETag() => word &= ~kETagBitMask; + public override readonly string ToString() { var paRC = IsReadCache(PreviousAddress) ? "(rc)" : string.Empty; static string bstr(bool value) => value ? "T" : "F"; return $"prev {AbsoluteAddress(PreviousAddress)}{paRC}, valid {bstr(Valid)}, tomb {bstr(Tombstone)}, seal {bstr(IsSealed)}," - + $" mod {bstr(Modified)}, dirty {bstr(Dirty)}, fill {bstr(HasFiller)}, Un1 {bstr(Unused1)}, Un2 {bstr(Unused2)}"; + + $" mod {bstr(Modified)}, dirty {bstr(Dirty)}, fill {bstr(HasFiller)}, etag {bstr(ETag)}, Un1 {bstr(Unused1)}"; } } } \ No newline at end of file diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalRMW.cs b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalRMW.cs index 0652f03c90..77f01fb663 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalRMW.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalRMW.cs @@ -443,6 +443,9 @@ private OperationStatus CreateNewRecordRMW public byte Value => (byte)statusCode; + /// + /// Whther the operation performed an update on the record or not + /// + public bool IsUpdated => Record.InPlaceUpdated || Record.CopyUpdated; + /// /// "Found" is zero, so does not appear in the output by default; this handles that explicitly public override string ToString() => (Found ? "Found, " : string.Empty) + statusCode.ToString(); diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs index 71b38bebc6..1cff20d69b 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs @@ -174,26 +174,32 @@ public bool Invalid /// /// Get Span<byte> for this 's payload (excluding metadata if any) + /// + /// Optional Parameter to avoid having to call slice when wanting to interact directly with payload skipping ETag at the front of the payload + /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AsSpan() + public Span AsSpan(int offset = 0) { if (Serialized) - return new Span(MetadataSize + (byte*)Unsafe.AsPointer(ref payload), Length - MetadataSize); + return new Span(MetadataSize + (byte*)Unsafe.AsPointer(ref payload) + offset, Length - MetadataSize - offset); else - return new Span(MetadataSize + (byte*)payload, Length - MetadataSize); + return new Span(MetadataSize + (byte*)payload + offset, Length - MetadataSize - offset); } /// /// Get ReadOnlySpan<byte> for this 's payload (excluding metadata if any) + /// + /// Optional Parameter to avoid having to call slice when wanting to interact directly with payload skipping ETag at the front of the payload + /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan AsReadOnlySpan() + public ReadOnlySpan AsReadOnlySpan(int offset = 0) { if (Serialized) - return new ReadOnlySpan(MetadataSize + (byte*)Unsafe.AsPointer(ref payload), Length - MetadataSize); + return new ReadOnlySpan(MetadataSize + (byte*)Unsafe.AsPointer(ref payload) + offset, Length - MetadataSize - offset); else - return new ReadOnlySpan(MetadataSize + (byte*)payload, Length - MetadataSize); + return new ReadOnlySpan(MetadataSize + (byte*)payload + offset, Length - MetadataSize - offset); } /// diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs new file mode 100644 index 0000000000..fa79411119 --- /dev/null +++ b/test/Garnet.test/RespEtagTests.cs @@ -0,0 +1,173 @@ + +using System.Text; +using Garnet.server; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using StackExchange.Redis; + +namespace Garnet.test +{ + [TestFixture] + public class RespEtagTests + { + private GarnetServer server; + + [SetUp] + public void Setup() + { + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true); + server.Start(); + } + + [TearDown] + public void TearDown() + { + server.Dispose(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir); + } + + #region ETAG SET Happy Paths + + [Test] + public void SetWithEtagReturnsEtagForNewData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + long etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); + } + + [Test] + public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var key = "florida"; + RedisResult res = (RedisResult)db.Execute("SETWITHETAG", [key, "one"]); + long initalEtag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, initalEtag); + + RedisResult[] setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", initalEtag]); + + long nextEtag = long.Parse(setIfMatchRes[0].ToString()); + string value = setIfMatchRes[1].ToString(); + + ClassicAssert.AreEqual(1, nextEtag); + ClassicAssert.AreEqual(value, "nextone"); + + setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextnextone", nextEtag]); + nextEtag = long.Parse(setIfMatchRes[0].ToString()); + value = setIfMatchRes[1].ToString(); + + ClassicAssert.AreEqual(2, nextEtag); + ClassicAssert.AreEqual(value, "nextnextone"); + + setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "lastOne", nextEtag]); + nextEtag = long.Parse(setIfMatchRes[0].ToString()); + value = setIfMatchRes[1].ToString(); + + ClassicAssert.AreEqual(3, nextEtag); + ClassicAssert.AreEqual(value, "lastOne"); + } + + #endregion + + #region ETAG GET Happy Paths + + [Test] + public void GetWithEtagReturnsValAndEtagForKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var key = "florida"; + // Data that does not exist returns nil + RedisResult nonExistingData = db.Execute("GETWITHETAG", [key]); + ClassicAssert.IsTrue(nonExistingData.IsNull); + + // insert data + var _ = db.Execute("SETWITHETAG", [key, "hkhalid"]); + + RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + long etag= long.Parse(res[0].ToString()); + string value = res[1].ToString(); + + ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual("hkhalid", value); + } + + [Test] + public void GetIfNotMatchReturnsDataWhenEtagDoesNotMatch() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var key = "florida"; + // GetIfNotMatch on non-existing data will return null + RedisResult nonExistingData = db.Execute("GETIFNOTMATCH", [key, 0]); + ClassicAssert.IsTrue(nonExistingData.IsNull); + + // insert data + var _ = db.Execute("SETWITHETAG", [key, "maximus"]); + + RedisResult noDataOnMatch = db.Execute("GETIFNOTMATCH", [key, 0]); + + ClassicAssert.AreEqual("NOTCHANGED", noDataOnMatch.ToString()); + + RedisResult[] res = (RedisResult[])db.Execute("GETIFNOTMATCH", [key, 1]); + long etag= long.Parse(res[0].ToString()); + string value = res[1].ToString(); + + ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual("maximus", value); + } + + #endregion + + #region ETAG Apis with non-etag data + + // ETAG Apis with non-Etag data just tests that in all scenarios we always return wrong data type response + [Test] + public void SetIfMatchOnNonEtagDataReturnsWrongType() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var _ = db.StringSet("h", "k"); + + RedisServerException ex = Assert.Throws(() => db.Execute("SETIFMATCH", ["h", "t", "0"])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + + ex = Assert.Throws(() => db.Execute("SETIFMATCH", ["h", "t", "1"])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + } + + [Test] + public void GetIfNotMatchOnNonEtagDataReturnsWrongType() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var _ = db.StringSet("h", "k"); + + RedisServerException ex = Assert.Throws(() => db.Execute("GETIFNOTMATCH", ["h", "0"])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + + ex = Assert.Throws(() => db.Execute("GETIFNOTMATCH", ["h", "1"])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + } + + #endregion + } +} \ No newline at end of file diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index ac5a60fff8..efa73b7003 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -258,6 +258,27 @@ public void LargeSetGet() ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); } + [Test] + public void LargeSetGetForEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + const int length = (1 << 19) + 100; + var value = new byte[length]; + + for (int i = 0; i < length; i++) + value[i] = (byte)((byte)'a' + ((byte)i % 26)); + + long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", value]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + + // Backwards compatability of data set with etag and plain GET call + var retvalue = (byte[])db.StringGet("mykey"); + + ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); + } + [Test] public void SetExpiry() { @@ -295,6 +316,51 @@ public void SetExpiry() ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); } + + [Test] + public void SetExpiryForEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string origValue = "abcdefghij"; + + // set with etag + long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", origValue]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + + // Expire the key in few seconds from now + ClassicAssert.IsTrue( + db.KeyExpire("mykey", TimeSpan.FromSeconds(2)) + ); + + string retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(origValue, retValue, "Get() before expiration"); + + var actualDbSize = db.Execute("DBSIZE"); + ClassicAssert.AreEqual(1, (ulong)actualDbSize, "DBSIZE before expiration"); + + var actualKeys = db.Execute("KEYS", ["*"]); + ClassicAssert.AreEqual(1, ((RedisResult[])actualKeys).Length, "KEYS before expiration"); + + var actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN before expiration"); + + Thread.Sleep(2500); + + retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(null, retValue, "Get() after expiration"); + + actualDbSize = db.Execute("DBSIZE"); + ClassicAssert.AreEqual(0, (ulong)actualDbSize, "DBSIZE after expiration"); + + actualKeys = db.Execute("KEYS", ["*"]); + ClassicAssert.AreEqual(0, ((RedisResult[])actualKeys).Length, "KEYS after expiration"); + + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); + } + [Test] public void SetExpiryHighPrecision() { @@ -431,6 +497,75 @@ public void SetGet() ClassicAssert.IsNull(expiry); } + [Test] + public void SetGetForEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string key = "mykey"; + string origValue = "abcdefghijklmnopqrst"; + + // Initial set + var _ = db.Execute("SETWITHETAG", [key, origValue]); + string retValue = db.StringGet(key); + ClassicAssert.AreEqual(origValue, retValue); + + // Smaller new value without expiration + string newValue1 = "abcdefghijklmnopqrs"; + retValue = db.StringSetAndGet(key, newValue1, null, When.Always, CommandFlags.None); + ClassicAssert.AreEqual(origValue, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue1, retValue); + + // HK TODO: This should increase the ETAG internally so add a check for that here + + // Smaller new value with KeepTtl + string newValue2 = "abcdefghijklmnopqr"; + retValue = db.StringSetAndGet(key, newValue2, null, true, When.Always, CommandFlags.None); + ClassicAssert.AreEqual(newValue1, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue2, retValue); + var expiry = db.KeyTimeToLive(key); + ClassicAssert.IsNull(expiry); + + // Smaller new value with expiration + string newValue3 = "01234"; + retValue = db.StringSetAndGet(key, newValue3, TimeSpan.FromSeconds(10), When.Exists, CommandFlags.None); + ClassicAssert.AreEqual(newValue2, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue3, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); + + // Larger new value with expiration + string newValue4 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; + retValue = db.StringSetAndGet(key, newValue4, TimeSpan.FromSeconds(100), When.Exists, CommandFlags.None); + ClassicAssert.AreEqual(newValue3, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue4, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); + + // Smaller new value without expiration + string newValue5 = "0123401234"; + retValue = db.StringSetAndGet(key, newValue5, null, When.Exists, CommandFlags.None); + ClassicAssert.AreEqual(newValue4, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue5, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsNull(expiry); + + // Larger new value without expiration + string newValue6 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; + retValue = db.StringSetAndGet(key, newValue6, null, When.Always, CommandFlags.None); + ClassicAssert.AreEqual(newValue5, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue6, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsNull(expiry); + } + [Test] public void SetExpiryIncr() From 74619d041266fc8b19cdb853f71aa1e6527a9ab5 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 20 Sep 2024 06:27:39 +1000 Subject: [PATCH 02/87] WIP tests --- test/Garnet.test/RespEtagTests.cs | 2046 ++++++++++++++++++++++++++ test/Garnet.test/RespTests.cs | 136 -- test/Garnet.test/TransactionTests.cs | 2 + 3 files changed, 2048 insertions(+), 136 deletions(-) diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index fa79411119..b1ddd2cb2a 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1,5 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Garnet.common; using Garnet.server; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -11,10 +18,12 @@ namespace Garnet.test public class RespEtagTests { private GarnetServer server; + private Random r; [SetUp] public void Setup() { + r = new Random(674386); TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true); server.Start(); @@ -169,5 +178,2042 @@ public void GetIfNotMatchOnNonEtagDataReturnsWrongType() } #endregion + + #region Backwards Compatability Testing + + [Test] + public void SingleEtagSetGet() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string origValue = "abcdefg"; + db.Execute("SETWITHETAG", ["mykey", origValue]); + + string retValue = db.StringGet("mykey"); + + ClassicAssert.AreEqual(origValue, retValue); + } + + [Test] + public async Task SingleUnicodeEtagSetGetGarnetClient() + { + using var db = TestUtils.GetGarnetClient(); + db.Connect(); + + string origValue = "笑い男"; + await db.ExecuteForLongResultAsync("SETWITHETAG", ["mykey", origValue]); + + string retValue = await db.StringGetAsync("mykey"); + + ClassicAssert.AreEqual(origValue, retValue); + } + + [Test] + public void LargeEtagSetGet() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + const int length = (1 << 19) + 100; + var value = new byte[length]; + + for (int i = 0; i < length; i++) + value[i] = (byte)((byte)'a' + ((byte)i % 26)); + + long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", value]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + + // Backwards compatability of data set with etag and plain GET call + var retvalue = (byte[])db.StringGet("mykey"); + + ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); + } + + [Test] + public void SetExpiryForEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string origValue = "abcdefghij"; + + // set with etag + long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", origValue]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + + // Expire the key in few seconds from now + ClassicAssert.IsTrue( + db.KeyExpire("mykey", TimeSpan.FromSeconds(2)) + ); + + string retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(origValue, retValue, "Get() before expiration"); + + var actualDbSize = db.Execute("DBSIZE"); + ClassicAssert.AreEqual(1, (ulong)actualDbSize, "DBSIZE before expiration"); + + var actualKeys = db.Execute("KEYS", ["*"]); + ClassicAssert.AreEqual(1, ((RedisResult[])actualKeys).Length, "KEYS before expiration"); + + var actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN before expiration"); + + Thread.Sleep(2500); + + retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(null, retValue, "Get() after expiration"); + + actualDbSize = db.Execute("DBSIZE"); + ClassicAssert.AreEqual(0, (ulong)actualDbSize, "DBSIZE after expiration"); + + actualKeys = db.Execute("KEYS", ["*"]); + ClassicAssert.AreEqual(0, ((RedisResult[])actualKeys).Length, "KEYS after expiration"); + + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); + } + + [Test] + public void SetExpiryHighPrecisionForEtagSetDatat() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var origValue = "abcdeghijklmno"; + // set with etag + long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", origValue]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + + // Expire the key in few seconds from now + db.KeyExpire("mykey", TimeSpan.FromSeconds(1.9)); + + string retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(origValue, retValue); + + Thread.Sleep(1000); + retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(origValue, retValue); + + Thread.Sleep(2000); + retValue = db.StringGet("mykey"); + ClassicAssert.AreEqual(null, retValue); + } + + // HK TODO: Keep working from here + + [Test] + public void SetGetForEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string key = "mykey"; + string origValue = "abcdefghijklmnopqrst"; + + // Initial set + var _ = db.Execute("SETWITHETAG", [key, origValue]); + string retValue = db.StringGet(key); + ClassicAssert.AreEqual(origValue, retValue); + + // Smaller new value without expiration + string newValue1 = "abcdefghijklmnopqrs"; + retValue = db.StringSetAndGet(key, newValue1, null, When.Always, CommandFlags.None); + ClassicAssert.AreEqual(origValue, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue1, retValue); + + // This should increase the ETAG internally so we have a check for that here + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(1, checkEtag); + + // Smaller new value with KeepTtl + string newValue2 = "abcdefghijklmnopqr"; + retValue = db.StringSetAndGet(key, newValue2, null, true, When.Always, CommandFlags.None); + ClassicAssert.AreEqual(newValue1, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue2, retValue); + var expiry = db.KeyTimeToLive(key); + ClassicAssert.IsNull(expiry); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(2, checkEtag); + + // Smaller new value with expiration + string newValue3 = "01234"; + retValue = db.StringSetAndGet(key, newValue3, TimeSpan.FromSeconds(10), When.Exists, CommandFlags.None); + ClassicAssert.AreEqual(newValue2, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue3, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(3, checkEtag); + + // Larger new value with expiration + string newValue4 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; + // HK TODO: WHY RETURNING NULL!? + retValue = db.StringSetAndGet(key, newValue4, TimeSpan.FromSeconds(100), When.Exists, CommandFlags.None); + ClassicAssert.AreEqual(newValue3, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue4, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(4, checkEtag); + + // Smaller new value without expiration + string newValue5 = "0123401234"; + retValue = db.StringSetAndGet(key, newValue5, null, When.Exists, CommandFlags.None); + ClassicAssert.AreEqual(newValue4, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue5, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsNull(expiry); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ]).ToString()); + ClassicAssert.AreEqual(4, checkEtag); + + // Larger new value without expiration + string newValue6 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; + retValue = db.StringSetAndGet(key, newValue6, null, When.Always, CommandFlags.None); + ClassicAssert.AreEqual(newValue5, retValue); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue6, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsNull(expiry); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ]).ToString()); + ClassicAssert.AreEqual(5, checkEtag); + } + + + [Test] + public void SetExpiryIncr() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Key storing integer + var nVal = -100000; + var strKey = "key1"; + db.StringSet(strKey, nVal, TimeSpan.FromSeconds(1)); + + long n = db.StringIncrement(strKey); + long nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(-99999, nRetVal); + + n = db.StringIncrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(-99998, nRetVal); + + Thread.Sleep(5000); + + // Expired key, restart increment + n = db.StringIncrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(1, nRetVal); + } + + [Test] + public void IncrDecrChangeDigitsWithExpiry() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Key storing integer + var strKey = "key1"; + db.StringSet(strKey, 9, TimeSpan.FromSeconds(1000)); + + long n = db.StringIncrement(strKey); + long nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(10, nRetVal); + + n = db.StringDecrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(9, nRetVal); + + db.StringSet(strKey, 99, TimeSpan.FromSeconds(1000)); + n = db.StringIncrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(100, nRetVal); + + n = db.StringDecrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(99, nRetVal); + + db.StringSet(strKey, 999, TimeSpan.FromSeconds(1000)); + n = db.StringIncrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(1000, nRetVal); + + n = db.StringDecrement(strKey); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + ClassicAssert.AreEqual(999, nRetVal); + } + + [Test] + public void SetOptionsCaseSensitivityTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "csKey"; + var value = 1; + var setCommand = "SET"; + var ttlCommand = "TTL"; + var okResponse = "OK"; + + // xx + var resp = (string)db.Execute($"{setCommand}", key, value, "xx"); + ClassicAssert.IsNull(resp); + + ClassicAssert.IsTrue(db.StringSet(key, value)); + + // nx + resp = (string)db.Execute($"{setCommand}", key, value, "nx"); + ClassicAssert.IsNull(resp); + + // ex + resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1"); + ClassicAssert.AreEqual(okResponse, resp); + Thread.Sleep(TimeSpan.FromSeconds(1.1)); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out var ttl)); + ClassicAssert.AreEqual(-2, ttl); + + // px + resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000"); + ClassicAssert.AreEqual(okResponse, resp); + Thread.Sleep(TimeSpan.FromSeconds(1.1)); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); + ClassicAssert.AreEqual(-2, ttl); + + // keepttl + ClassicAssert.IsTrue(db.StringSet(key, 1, TimeSpan.FromMinutes(1))); + resp = (string)db.Execute($"{setCommand}", key, value, "keepttl"); + ClassicAssert.AreEqual(okResponse, resp); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out ttl) && ttl > 0 && ttl < 60); + + // ex .. nx, non-existing key + ClassicAssert.IsTrue(db.KeyDelete(key)); + resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "nx"); + ClassicAssert.AreEqual(okResponse, resp); + Thread.Sleep(TimeSpan.FromSeconds(1.1)); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); + ClassicAssert.AreEqual(-2, ttl); + + // ex .. nx, existing key + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "nx"); + ClassicAssert.IsNull(resp); + + // ex .. xx, non-existing key + ClassicAssert.IsTrue(db.KeyDelete(key)); + resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "xx"); + ClassicAssert.IsNull(resp); + + // ex .. xx, existing key + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "xx"); + ClassicAssert.AreEqual(okResponse, resp); + Thread.Sleep(TimeSpan.FromSeconds(1.1)); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); + ClassicAssert.AreEqual(-2, ttl); + + // px .. nx, non-existing key + ClassicAssert.IsTrue(db.KeyDelete(key)); + resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "nx"); + ClassicAssert.AreEqual(okResponse, resp); + Thread.Sleep(TimeSpan.FromSeconds(1.1)); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); + ClassicAssert.AreEqual(-2, ttl); + + // px .. nx, existing key + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "nx"); + ClassicAssert.IsNull(resp); + + // px .. xx, non-existing key + ClassicAssert.IsTrue(db.KeyDelete(key)); + resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "xx"); + ClassicAssert.IsNull(resp); + + // px .. xx, existing key + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "xx"); + ClassicAssert.AreEqual(okResponse, resp); + Thread.Sleep(TimeSpan.FromSeconds(1.1)); + resp = (string)db.Execute($"{ttlCommand}", key); + ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); + ClassicAssert.AreEqual(-2, ttl); + } + + [Test] + public void LockTakeRelease() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string key = "lock-key"; + string value = "lock-value"; + + var success = db.LockTake(key, value, TimeSpan.FromSeconds(100)); + ClassicAssert.IsTrue(success); + + success = db.LockTake(key, value, TimeSpan.FromSeconds(100)); + ClassicAssert.IsFalse(success); + + success = db.LockRelease(key, value); + ClassicAssert.IsTrue(success); + + success = db.LockRelease(key, value); + ClassicAssert.IsFalse(success); + + success = db.LockTake(key, value, TimeSpan.FromSeconds(100)); + ClassicAssert.IsTrue(success); + + success = db.LockRelease(key, value); + ClassicAssert.IsTrue(success); + + // Test auto-lock-release + success = db.LockTake(key, value, TimeSpan.FromSeconds(1)); + ClassicAssert.IsTrue(success); + + Thread.Sleep(2000); + success = db.LockTake(key, value, TimeSpan.FromSeconds(1)); + ClassicAssert.IsTrue(success); + + success = db.LockRelease(key, value); + ClassicAssert.IsTrue(success); + } + + [Test] + [TestCase(10)] + [TestCase(50)] + [TestCase(100)] + public void SingleIncr(int bytesPerSend) + { + using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); + + // Key storing integer + var nVal = -100000; + var strKey = "key1"; + + var expectedResponse = "+OK\r\n"; + var response = lightClientRequest.Execute($"SET {strKey} {nVal}", expectedResponse.Length, bytesPerSend); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = "$7\r\n-100000\r\n"; + response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesPerSend); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = ":-99999\r\n"; + response = lightClientRequest.Execute($"INCR {strKey}", expectedResponse.Length, bytesPerSend); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = "$6\r\n-99999\r\n"; + response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesPerSend); + ClassicAssert.AreEqual(expectedResponse, response); + } + + [Test] + [TestCase(9999, 10)] + [TestCase(9999, 50)] + [TestCase(9999, 100)] + public void SingleIncrBy(long nIncr, int bytesSent) + { + using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); + + // Key storing integer + var nVal = 1000; + var strKey = "key1"; + + var expectedResponse = "+OK\r\n"; + var response = lightClientRequest.Execute($"SET {strKey} {nVal}", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = "$4\r\n1000\r\n"; + response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = $":{nIncr + nVal}\r\n"; + response = lightClientRequest.Execute($"INCRBY {strKey} {nIncr}", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = $"${(nIncr + nVal).ToString().Length}\r\n{nIncr + nVal}\r\n"; + response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + } + + [Test] + [TestCase("key1", 1000)] + [TestCase("key1", 0)] + public void SingleDecr(string strKey, int nVal) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Key storing integer + db.StringSet(strKey, nVal); + long n = db.StringDecrement(strKey); + ClassicAssert.AreEqual(nVal - 1, n); + long nRetVal = Convert.ToInt64(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + } + + [Test] + [TestCase(-1000, 100)] + [TestCase(-1000, -9000)] + [TestCase(-10000, 9000)] + [TestCase(9000, 10000)] + public void SingleDecrBy(long nVal, long nDecr) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + // Key storing integer val + var strKey = "key1"; + db.StringSet(strKey, nVal); + long n = db.StringDecrement(strKey, nDecr); + + int nRetVal = Convert.ToInt32(db.StringGet(strKey)); + ClassicAssert.AreEqual(n, nRetVal); + } + + [Test] + public void SingleDecrByNoKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + long decrBy = 1000; + + // Key storing integer + var strKey = "key1"; + db.StringDecrement(strKey, decrBy); + + var retValStr = db.StringGet(strKey).ToString(); + int retVal = Convert.ToInt32(retValStr); + + ClassicAssert.AreEqual(-decrBy, retVal); + } + + [Test] + public void SingleIncrNoKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Key storing integer + var strKey = "key1"; + db.StringIncrement(strKey); + + int retVal = Convert.ToInt32(db.StringGet(strKey)); + + ClassicAssert.AreEqual(1, retVal); + + // Key storing integer + strKey = "key2"; + db.StringDecrement(strKey); + + retVal = Convert.ToInt32(db.StringGet(strKey)); + + ClassicAssert.AreEqual(-1, retVal); + } + + [Test] + [TestCase(RespCommand.INCR, true)] + [TestCase(RespCommand.DECR, true)] + [TestCase(RespCommand.INCRBY, true)] + [TestCase(RespCommand.DECRBY, true)] + [TestCase(RespCommand.INCRBY, false)] + [TestCase(RespCommand.DECRBY, false)] + public void SimpleIncrementInvalidValue(RespCommand cmd, bool initialize) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + string[] values = ["", "7 3", "02+(34", "笑い男", "01", "-01", "7ab"]; + + for (var i = 0; i < values.Length; i++) + { + var key = $"key{i}"; + var exception = false; + if (initialize) + { + var resp = db.StringSet(key, values[i]); + ClassicAssert.AreEqual(true, resp); + } + try + { + _ = cmd switch + { + RespCommand.INCR => db.StringIncrement(key), + RespCommand.DECR => db.StringDecrement(key), + RespCommand.INCRBY => initialize ? db.StringIncrement(key, 10L) : (long)db.Execute("INCRBY", [key, values[i]]), + RespCommand.DECRBY => initialize ? db.StringDecrement(key, 10L) : (long)db.Execute("DECRBY", [key, values[i]]), + _ => throw new Exception($"Command {cmd} not supported!"), + }; + } + catch (Exception ex) + { + exception = true; + var msg = ex.Message; + ClassicAssert.AreEqual("ERR value is not an integer or out of range.", msg); + } + ClassicAssert.IsTrue(exception); + } + } + + [Test] + [TestCase(RespCommand.INCR)] + [TestCase(RespCommand.DECR)] + [TestCase(RespCommand.INCRBY)] + [TestCase(RespCommand.DECRBY)] + public void SimpleIncrementOverflow(RespCommand cmd) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var exception = false; + + var key = "test"; + try + { + switch (cmd) + { + case RespCommand.INCR: + _ = db.StringSet(key, long.MaxValue.ToString()); + _ = db.StringIncrement(key); + break; + case RespCommand.DECR: + _ = db.StringSet(key, long.MinValue.ToString()); + _ = db.StringDecrement(key); + break; + case RespCommand.INCRBY: + _ = db.Execute("INCRBY", [key, ulong.MaxValue.ToString()]); + break; + case RespCommand.DECRBY: + _ = db.Execute("DECRBY", [key, ulong.MaxValue.ToString()]); + break; + } + } + catch (Exception ex) + { + exception = true; + var msg = ex.Message; + ClassicAssert.AreEqual("ERR value is not an integer or out of range.", msg); + } + ClassicAssert.IsTrue(exception); + } + + [Test] + public void SingleDelete() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Key storing integer + var nVal = 100; + var strKey = "key1"; + db.StringSet(strKey, nVal); + db.KeyDelete(strKey); + var retVal = Convert.ToBoolean(db.StringGet(strKey)); + ClassicAssert.AreEqual(retVal, false); + } + + [Test] + public void SingleDeleteWithObjectStoreDisabled() + { + TearDown(); + + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, DisableObjects: true); + server.Start(); + + var key = "delKey"; + var value = "1234"; + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + db.StringSet(key, value); + + var resp = (string)db.StringGet(key); + ClassicAssert.AreEqual(resp, value); + + var respDel = db.KeyDelete(key); + ClassicAssert.IsTrue(respDel); + + respDel = db.KeyDelete(key); + ClassicAssert.IsFalse(respDel); + } + + private string GetRandomString(int len) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, len) + .Select(s => s[r.Next(s.Length)]).ToArray()); + } + + [Test] + public void SingleDeleteWithObjectStoreDisable_LTM() + { + TearDown(); + + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true, DisableObjects: true); + server.Start(); + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int keyCount = 5; + int valLen = 256; + int keyLen = 8; + + List> data = []; + for (int i = 0; i < keyCount; i++) + { + data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); + var pair = data.Last(); + db.StringSet(pair.Item1, pair.Item2); + } + + + for (int i = 0; i < keyCount; i++) + { + var pair = data[i]; + + var resp = (string)db.StringGet(pair.Item1); + ClassicAssert.AreEqual(resp, pair.Item2); + + var respDel = db.KeyDelete(pair.Item1); + resp = (string)db.StringGet(pair.Item1); + ClassicAssert.IsNull(resp); + + respDel = db.KeyDelete(pair.Item2); + ClassicAssert.IsFalse(respDel); + } + } + + [Test] + public void MultiKeyDelete([Values] bool withoutObjectStore) + { + if (withoutObjectStore) + { + TearDown(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, DisableObjects: true); + server.Start(); + } + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int keyCount = 10; + int valLen = 16; + int keyLen = 8; + + List> data = []; + for (int i = 0; i < keyCount; i++) + { + data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); + var pair = data.Last(); + db.StringSet(pair.Item1, pair.Item2); + } + + var keys = data.Select(x => (RedisKey)x.Item1).ToArray(); + var keysDeleted = db.KeyDeleteAsync(keys); + keysDeleted.Wait(); + ClassicAssert.AreEqual(keysDeleted.Result, 10); + + var keysDel = db.KeyDelete(keys); + ClassicAssert.AreEqual(keysDel, 0); + } + + [Test] + public void MultiKeyDeleteObjectStore() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int keyCount = 10; + int setCount = 3; + int valLen = 16; + int keyLen = 8; + + List keys = []; + for (int i = 0; i < keyCount; i++) + { + keys.Add(GetRandomString(keyLen)); + var key = keys.Last(); + + for (int j = 0; j < setCount; j++) + { + var member = GetRandomString(valLen); + db.SetAdd(key, member); + } + } + + var redisKeys = keys.Select(x => (RedisKey)x).ToArray(); + var keysDeleted = db.KeyDeleteAsync(redisKeys); + keysDeleted.Wait(); + ClassicAssert.AreEqual(keysDeleted.Result, 10); + + var keysDel = db.KeyDelete(redisKeys); + ClassicAssert.AreEqual(keysDel, 0); + } + + [Test] + public void MultiKeyUnlink([Values] bool withoutObjectStore) + { + if (withoutObjectStore) + { + TearDown(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, DisableObjects: true); + server.Start(); + } + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int keyCount = 10; + int valLen = 16; + int keyLen = 8; + + List> data = []; + for (int i = 0; i < keyCount; i++) + { + data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); + var pair = data.Last(); + db.StringSet(pair.Item1, pair.Item2); + } + + var keys = data.Select(x => (object)x.Item1).ToArray(); + var keysDeleted = (string)db.Execute("unlink", keys); + ClassicAssert.AreEqual(10, int.Parse(keysDeleted)); + + keysDeleted = (string)db.Execute("unlink", keys); + ClassicAssert.AreEqual(0, int.Parse(keysDeleted)); + } + + [Test] + public void MultiKeyUnlinkObjectStore() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int keyCount = 10; + int setCount = 3; + int valLen = 16; + int keyLen = 8; + + List keys = []; + for (int i = 0; i < keyCount; i++) + { + keys.Add(GetRandomString(keyLen)); + var key = keys.Last(); + + for (int j = 0; j < setCount; j++) + { + var member = GetRandomString(valLen); + db.SetAdd(key, member); + } + } + + var redisKey = keys.Select(x => (object)x).ToArray(); + var keysDeleted = (string)db.Execute("unlink", redisKey); + ClassicAssert.AreEqual(Int32.Parse(keysDeleted), 10); + + keysDeleted = (string)db.Execute("unlink", redisKey); + ClassicAssert.AreEqual(Int32.Parse(keysDeleted), 0); + } + + [Test] + public void SingleExists([Values] bool withoutObjectStore) + { + if (withoutObjectStore) + { + TearDown(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, DisableObjects: true); + server.Start(); + } + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Key storing integer + var nVal = 100; + var strKey = "key1"; + ClassicAssert.IsFalse(db.KeyExists(strKey)); + db.StringSet(strKey, nVal); + + bool fExists = db.KeyExists("key1", CommandFlags.None); + ClassicAssert.AreEqual(fExists, true); + + fExists = db.KeyExists("key2", CommandFlags.None); + ClassicAssert.AreEqual(fExists, false); + } + + [Test] + public void SingleExistsObject() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "key"; + ClassicAssert.IsFalse(db.KeyExists(key)); + + var listData = new RedisValue[] { "a", "b", "c", "d" }; + var count = db.ListLeftPush(key, listData); + ClassicAssert.AreEqual(4, count); + ClassicAssert.True(db.KeyExists(key)); + } + + [Test] + public void MultipleExistsKeysAndObjects() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var count = db.ListLeftPush("listKey", ["a", "b", "c", "d"]); + ClassicAssert.AreEqual(4, count); + + var zaddItems = db.SortedSetAdd("zset:test", [new SortedSetEntry("a", 1), new SortedSetEntry("b", 2)]); + ClassicAssert.AreEqual(2, zaddItems); + + db.StringSet("foo", "bar"); + + var exists = db.KeyExists(["key", "listKey", "zset:test", "foo"]); + ClassicAssert.AreEqual(3, exists); + } + + + [Test] + public void SingleRename() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string origValue = "test1"; + db.StringSet("key1", origValue); + + db.KeyRename("key1", "key2"); + string retValue = db.StringGet("key2"); + + ClassicAssert.AreEqual(origValue, retValue); + + origValue = db.StringGet("key1"); + ClassicAssert.AreEqual(null, origValue); + } + + [Test] + public void SingleRenameKeyEdgeCase([Values] bool withoutObjectStore) + { + if (withoutObjectStore) + { + TearDown(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, DisableObjects: true); + server.Start(); + } + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + //1. Key rename does not exist + try + { + var res = db.KeyRename("key1", "key2"); + } + catch (Exception ex) + { + ClassicAssert.AreEqual("ERR no such key", ex.Message); + } + + //2. Key rename oldKey.Equals(newKey) + string origValue = "test1"; + db.StringSet("key1", origValue); + bool renameRes = db.KeyRename("key1", "key1"); + ClassicAssert.IsTrue(renameRes); + string retValue = db.StringGet("key1"); + ClassicAssert.AreEqual(origValue, retValue); + } + + [Test] + public void SingleRenameObjectStore() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var origList = new RedisValue[] { "a", "b", "c", "d" }; + var key1 = "lkey1"; + var count = db.ListRightPush(key1, origList); + ClassicAssert.AreEqual(4, count); + + var result = db.ListRange(key1); + ClassicAssert.AreEqual(origList, result); + + var key2 = "lkey2"; + var rb = db.KeyRename(key1, key2); + ClassicAssert.IsTrue(rb); + result = db.ListRange(key1); + ClassicAssert.AreEqual(Array.Empty(), result); + + result = db.ListRange(key2); + ClassicAssert.AreEqual(origList, result); + } + + [Test] + public void CanSelectCommand() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var reply = db.Execute("SELECT", "0"); + ClassicAssert.IsTrue(reply.ToString() == "OK"); + Assert.Throws(() => db.Execute("SELECT", "1")); + + //select again the def db + db.Execute("SELECT", "0"); + } + + [Test] + public void CanSelectCommandLC() + { + using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); + + var expectedResponse = "-ERR invalid database index.\r\n+PONG\r\n"; + var response = lightClientRequest.Execute("SELECT 1", "PING", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + } + + [Test] + [TestCase(10)] + [TestCase(50)] + [TestCase(100)] + public void CanDoCommandsInChunks(int bytesSent) + { + // SETEX + using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); + + var expectedResponse = "+OK\r\n"; + var response = lightClientRequest.Execute("SETEX mykey 1 abcdefghij", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // GET + expectedResponse = "$10\r\nabcdefghij\r\n"; + response = lightClientRequest.Execute("GET mykey", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + Thread.Sleep(2000); + + // GET + expectedResponse = "$-1\r\n"; + response = lightClientRequest.Execute("GET mykey", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // DECR + expectedResponse = "+OK\r\n"; + response = lightClientRequest.Execute("SET mykeydecr 1", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = ":0\r\n"; + response = lightClientRequest.Execute("DECR mykeydecr", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = "$1\r\n0\r\n"; + response = lightClientRequest.Execute("GET mykeydecr", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // DEL + expectedResponse = ":1\r\n"; + response = lightClientRequest.Execute("DEL mykeydecr", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = "$-1\r\n"; + response = lightClientRequest.Execute("GET mykeydecr", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // EXISTS + expectedResponse = ":0\r\n"; + response = lightClientRequest.Execute("EXISTS mykeydecr", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // SET + expectedResponse = "+OK\r\n"; + response = lightClientRequest.Execute("SET mykey 1", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // RENAME + expectedResponse = "+OK\r\n"; + response = lightClientRequest.Execute("RENAME mykey mynewkey", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + // GET + expectedResponse = "$1\r\n1\r\n"; + response = lightClientRequest.Execute("GET mynewkey", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + } + + + [Test] + [TestCase(10)] + [TestCase(50)] + [TestCase(100)] + public void CanSetGetCommandsChunks(int bytesSent) + { + using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); + var sb = new StringBuilder(); + + for (int i = 1; i <= 100; i++) + { + sb.Append($" mykey-{i} {i * 10}"); + } + + // MSET + var expectedResponse = "+OK\r\n"; + var response = lightClientRequest.Execute($"MSET{sb}", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + expectedResponse = ":100\r\n"; + response = lightClientRequest.Execute($"DBSIZE", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + + sb.Clear(); + for (int i = 1; i <= 100; i++) + { + sb.Append($" mykey-{i}"); + } + + // MGET + expectedResponse = "*100\r\n$2\r\n10\r\n$2\r\n20\r\n$2\r\n30\r\n$2\r\n40\r\n$2\r\n50\r\n$2\r\n60\r\n$2\r\n70\r\n$2\r\n80\r\n$2\r\n90\r\n$3\r\n100\r\n$3\r\n110\r\n$3\r\n120\r\n$3\r\n130\r\n$3\r\n140\r\n$3\r\n150\r\n$3\r\n160\r\n$3\r\n170\r\n$3\r\n180\r\n$3\r\n190\r\n$3\r\n200\r\n$3\r\n210\r\n$3\r\n220\r\n$3\r\n230\r\n$3\r\n240\r\n$3\r\n250\r\n$3\r\n260\r\n$3\r\n270\r\n$3\r\n280\r\n$3\r\n290\r\n$3\r\n300\r\n$3\r\n310\r\n$3\r\n320\r\n$3\r\n330\r\n$3\r\n340\r\n$3\r\n350\r\n$3\r\n360\r\n$3\r\n370\r\n$3\r\n380\r\n$3\r\n390\r\n$3\r\n400\r\n$3\r\n410\r\n$3\r\n420\r\n$3\r\n430\r\n$3\r\n440\r\n$3\r\n450\r\n$3\r\n460\r\n$3\r\n470\r\n$3\r\n480\r\n$3\r\n490\r\n$3\r\n500\r\n$3\r\n510\r\n$3\r\n520\r\n$3\r\n530\r\n$3\r\n540\r\n$3\r\n550\r\n$3\r\n560\r\n$3\r\n570\r\n$3\r\n580\r\n$3\r\n590\r\n$3\r\n600\r\n$3\r\n610\r\n$3\r\n620\r\n$3\r\n630\r\n$3\r\n640\r\n$3\r\n650\r\n$3\r\n660\r\n$3\r\n670\r\n$3\r\n680\r\n$3\r\n690\r\n$3\r\n700\r\n$3\r\n710\r\n$3\r\n720\r\n$3\r\n730\r\n$3\r\n740\r\n$3\r\n750\r\n$3\r\n760\r\n$3\r\n770\r\n$3\r\n780\r\n$3\r\n790\r\n$3\r\n800\r\n$3\r\n810\r\n$3\r\n820\r\n$3\r\n830\r\n$3\r\n840\r\n$3\r\n850\r\n$3\r\n860\r\n$3\r\n870\r\n$3\r\n880\r\n$3\r\n890\r\n$3\r\n900\r\n$3\r\n910\r\n$3\r\n920\r\n$3\r\n930\r\n$3\r\n940\r\n$3\r\n950\r\n$3\r\n960\r\n$3\r\n970\r\n$3\r\n980\r\n$3\r\n990\r\n$4\r\n1000\r\n"; + response = lightClientRequest.Execute($"MGET{sb}", expectedResponse.Length, bytesSent); + ClassicAssert.AreEqual(expectedResponse, response); + } + + [Test] + public void PersistTTLTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "expireKey"; + var val = "expireValue"; + var expire = 2; + + var ttl = db.Execute("TTL", key); + ClassicAssert.AreEqual(-2, (int)ttl); + + db.StringSet(key, val); + ttl = db.Execute("TTL", key); + ClassicAssert.AreEqual(-1, (int)ttl); + + db.KeyExpire(key, TimeSpan.FromSeconds(expire)); + + var time = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + + db.KeyExpire(key, TimeSpan.FromSeconds(expire)); + db.KeyPersist(key); + + Thread.Sleep((expire + 1) * 1000); + + var _val = db.StringGet(key); + ClassicAssert.AreEqual(val, _val.ToString()); + + time = db.KeyTimeToLive(key); + ClassicAssert.IsNull(time); + } + + [Test] + public void ObjectTTLTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "expireKey"; + var expire = 2; + + var ttl = db.Execute("TTL", key); + ClassicAssert.AreEqual(-2, (int)ttl); + + db.SortedSetAdd(key, key, 1.0); + ttl = db.Execute("TTL", key); + ClassicAssert.AreEqual(-1, (int)ttl); + + db.KeyExpire(key, TimeSpan.FromSeconds(expire)); + + var time = db.KeyTimeToLive(key); + ClassicAssert.IsNotNull(time); + ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + } + + [Test] + public void PersistTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int expire = 100; + var keyA = "keyA"; + db.StringSet(keyA, keyA); + var response = db.KeyPersist(keyA); + ClassicAssert.IsFalse(response); + + db.KeyExpire(keyA, TimeSpan.FromSeconds(expire)); + var time = db.KeyTimeToLive(keyA); + ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + + response = db.KeyPersist(keyA); + ClassicAssert.IsTrue(response); + + time = db.KeyTimeToLive(keyA); + ClassicAssert.IsTrue(time == null); + + var value = db.StringGet(keyA); + ClassicAssert.AreEqual(value, keyA); + + var noKey = "noKey"; + response = db.KeyPersist(noKey); + ClassicAssert.IsFalse(response); + } + + [Test] + public void PersistObjectTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int expire = 100; + var keyA = "keyA"; + db.SortedSetAdd(keyA, [new SortedSetEntry("element", 1.0)]); + var response = db.KeyPersist(keyA); + ClassicAssert.IsFalse(response); + + db.KeyExpire(keyA, TimeSpan.FromSeconds(expire)); + var time = db.KeyTimeToLive(keyA); + ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + + response = db.KeyPersist(keyA); + ClassicAssert.IsTrue(response); + + time = db.KeyTimeToLive(keyA); + ClassicAssert.IsTrue(time == null); + + var value = db.SortedSetScore(keyA, "element"); + ClassicAssert.AreEqual(1.0, value); + } + + [Test] + [TestCase("EXPIRE")] + [TestCase("PEXPIRE")] + public void KeyExpireStringTest(string command) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "keyA"; + db.StringSet(key, key); + + var value = db.StringGet(key); + ClassicAssert.AreEqual(key, (string)value); + + if (command.Equals("EXPIRE")) + db.KeyExpire(key, TimeSpan.FromSeconds(1)); + else + db.Execute(command, [key, 1000]); + + Thread.Sleep(1500); + + value = db.StringGet(key); + ClassicAssert.AreEqual(null, (string)value); + } + + [Test] + [TestCase("EXPIRE")] + [TestCase("PEXPIRE")] + public void KeyExpireObjectTest(string command) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "keyA"; + db.SortedSetAdd(key, [new SortedSetEntry("element", 1.0)]); + + var value = db.SortedSetScore(key, "element"); + ClassicAssert.AreEqual(1.0, value, "Get Score before expiration"); + + var actualDbSize = db.Execute("DBSIZE"); + ClassicAssert.AreEqual(1, (ulong)actualDbSize, "DBSIZE before expiration"); + + var actualKeys = db.Execute("KEYS", ["*"]); + ClassicAssert.AreEqual(1, ((RedisResult[])actualKeys).Length, "KEYS before expiration"); + + var actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN before expiration"); + + var exp = db.KeyExpire(key, command.Equals("EXPIRE") ? TimeSpan.FromSeconds(1) : TimeSpan.FromMilliseconds(1000)); + ClassicAssert.IsTrue(exp); + + // Sleep to wait for expiration + Thread.Sleep(1500); + + value = db.SortedSetScore(key, "element"); + ClassicAssert.AreEqual(null, value, "Get Score after expiration"); + + actualDbSize = db.Execute("DBSIZE"); + ClassicAssert.AreEqual(0, (ulong)actualDbSize, "DBSIZE after expiration"); + + actualKeys = db.Execute("KEYS", ["*"]); + ClassicAssert.AreEqual(0, ((RedisResult[])actualKeys).Length, "KEYS after expiration"); + + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); + } + + [Test] + [TestCase("EXPIRE")] + [TestCase("PEXPIRE")] + public void KeyExpireOptionsTest(string command) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "keyA"; + object[] args = [key, 1000, ""]; + db.StringSet(key, key); + + args[2] = "XX";// XX -- Set expiry only when the key has an existing expiry + bool resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsFalse(resp);//XX return false no existing expiry + + args[2] = "NX";// NX -- Set expiry only when the key has no expiry + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsTrue(resp);// NX return true no existing expiry + + args[2] = "NX";// NX -- Set expiry only when the key has no expiry + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsFalse(resp);// NX return false existing expiry + + args[1] = 50; + args[2] = "XX";// XX -- Set expiry only when the key has an existing expiry + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsTrue(resp);// XX return true existing expiry + var time = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(time.Value.TotalSeconds <= (double)((int)args[1]) && time.Value.TotalSeconds > 0); + + args[1] = 1; + args[2] = "GT";// GT -- Set expiry only when the new expiry is greater than current one + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsFalse(resp); // GT return false new expiry < current expiry + + args[1] = 1000; + args[2] = "GT";// GT -- Set expiry only when the new expiry is greater than current one + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsTrue(resp); // GT return true new expiry > current expiry + time = db.KeyTimeToLive(key); + + if (command.Equals("EXPIRE")) + ClassicAssert.IsTrue(time.Value.TotalSeconds > 500); + else + ClassicAssert.IsTrue(time.Value.TotalMilliseconds > 500); + + args[1] = 2000; + args[2] = "LT";// LT -- Set expiry only when the new expiry is less than current one + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsFalse(resp); // LT return false new expiry > current expiry + + args[1] = 15; + args[2] = "LT";// LT -- Set expiry only when the new expiry is less than current one + resp = (bool)db.Execute($"{command}", args); + ClassicAssert.IsTrue(resp); // LT return true new expiry < current expiry + time = db.KeyTimeToLive(key); + + if (command.Equals("EXPIRE")) + ClassicAssert.IsTrue(time.Value.TotalSeconds <= (double)((int)args[1]) && time.Value.TotalSeconds > 0); + else + ClassicAssert.IsTrue(time.Value.TotalMilliseconds <= (double)((int)args[1]) && time.Value.TotalMilliseconds > 0); + } + + [Test] + public async Task ReAddExpiredKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + const string key = "x:expire_trap"; + + // Set + { + db.KeyDelete(key); + db.SetAdd(key, "v1"); + + ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial add"); + ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial add"); + var actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial ADD"); + + db.KeyExpire(key, TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(2)); + + ClassicAssert.IsFalse(db.KeyExists(key), $"KeyExists after expiration"); + ClassicAssert.AreEqual("0", db.Execute("EXISTS", key).ToString(), "EXISTS after ADD expiration"); + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after ADD expiration"); + + db.SetAdd(key, "v2"); + + ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial re-ADD"); + ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial re-ADD"); + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial re-ADD"); + } + // List + { + db.KeyDelete(key); + db.ListRightPush(key, "v1"); + + ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial RPUSH"); + ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial RPUSH"); + var actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial RPUSH"); + + db.KeyExpire(key, TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(2)); + + ClassicAssert.IsFalse(db.KeyExists(key), $"KeyExists after expiration"); + ClassicAssert.AreEqual("0", db.Execute("EXISTS", key).ToString(), "EXISTS after RPUSH expiration"); + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after RPUSH expiration"); + + db.ListRightPush(key, "v2"); + + ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial re-RPUSH"); + ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial re-RPUSH"); + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial re-RPUSH"); + } + // Hash + { + db.KeyDelete(key); + db.HashSet(key, "f1", "v1"); + + ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial HSET"); + ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial HSET"); + var actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial HSET"); + + db.KeyExpire(key, TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(2)); + + ClassicAssert.IsFalse(db.KeyExists(key), $"KeyExists after expiration"); + ClassicAssert.AreEqual("0", db.Execute("EXISTS", key).ToString(), "EXISTS after HSET expiration"); + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after HSET expiration"); + + db.HashSet(key, "f1", "v2"); + + ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial re-HSET"); + ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial re-HSET"); + actualScan = db.Execute("SCAN", "0"); + ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial re-HSET"); + } + } + + [Test] + public void MainObjectKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var server = redis.GetServers()[0]; + var db = redis.GetDatabase(0); + + const string key = "test:1"; + + // Do StringSet + ClassicAssert.IsTrue(db.StringSet(key, "v1")); + + // Do SetAdd using the same key + ClassicAssert.IsTrue(db.SetAdd(key, "v2")); + + // Two keys "test:1" - this is expected as of now + // because Garnet has a separate main and object store + var keys = server.Keys(db.Database, key).ToList(); + ClassicAssert.AreEqual(2, keys.Count); + ClassicAssert.AreEqual(key, (string)keys[0]); + ClassicAssert.AreEqual(key, (string)keys[1]); + + // do ListRightPush using the same key, expected error + var ex = Assert.Throws(() => db.ListRightPush(key, "v3")); + var expectedError = Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE); + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(expectedError, ex.Message); + } + + [Test] + public void GetSliceTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string key = "rangeKey"; + string value = "0123456789"; + + var resp = (string)db.StringGetRange(key, 2, 10); + ClassicAssert.AreEqual(string.Empty, resp); + ClassicAssert.AreEqual(true, db.StringSet(key, value)); + + //0,0 + resp = (string)db.StringGetRange(key, 0, 0); + ClassicAssert.AreEqual("0", resp); + + //actual value + resp = (string)db.StringGetRange(key, 0, -1); + ClassicAssert.AreEqual(value, resp); + + #region testA + //s[2,len] s < e & e = len + resp = (string)db.StringGetRange(key, 2, 10); + ClassicAssert.AreEqual(value.Substring(2), resp); + + //s[2,len] s < e & e = len - 1 + resp = (string)db.StringGetRange(key, 2, 9); + ClassicAssert.AreEqual(value.Substring(2), resp); + + //s[2,len] s < e < len + resp = (string)db.StringGetRange(key, 2, 5); + ClassicAssert.AreEqual(value.Substring(2, 4), resp); + + //s[2,len] s < len < e + resp = (string)db.StringGetRange(key, 2, 15); + ClassicAssert.AreEqual(value.Substring(2), resp); + + //s[4,len] e < s < len + resp = (string)db.StringGetRange(key, 4, 2); + ClassicAssert.AreEqual("", resp); + + //s[4,len] e < 0 < s < len + resp = (string)db.StringGetRange(key, 4, -2); + ClassicAssert.AreEqual(value.Substring(4, 5), resp); + + //s[4,len] e < -len < 0 < s < len + resp = (string)db.StringGetRange(key, 4, -12); + ClassicAssert.AreEqual("", resp); + #endregion + + #region testB + //-len < s < 0 < len < e + resp = (string)db.StringGetRange(key, -4, 15); + ClassicAssert.AreEqual(value.Substring(6, 4), resp); + + //-len < s < 0 < e < len where len + s > e + resp = (string)db.StringGetRange(key, -4, 5); + ClassicAssert.AreEqual("", resp); + + //-len < s < 0 < e < len where len + s < e + resp = (string)db.StringGetRange(key, -4, 8); + ClassicAssert.AreEqual(value.Substring(value.Length - 4, 2), resp); + + //-len < s < e < 0 + resp = (string)db.StringGetRange(key, -4, -1); + ClassicAssert.AreEqual(value.Substring(value.Length - 4, 4), resp); + + //-len < e < s < 0 + resp = (string)db.StringGetRange(key, -4, -7); + ClassicAssert.AreEqual("", resp); + #endregion + + //range start > end > len + resp = (string)db.StringGetRange(key, 17, 13); + ClassicAssert.AreEqual("", resp); + + //range 0 > start > end + resp = (string)db.StringGetRange(key, -1, -4); + ClassicAssert.AreEqual("", resp); + + //equal offsets + resp = db.StringGetRange(key, 4, 4); + ClassicAssert.AreEqual("4", resp); + + //equal offsets + resp = db.StringGetRange(key, -4, -4); + ClassicAssert.AreEqual("6", resp); + + //equal offsets + resp = db.StringGetRange(key, -100, -100); + ClassicAssert.AreEqual("0", resp); + + //equal offsets + resp = db.StringGetRange(key, -101, -101); + ClassicAssert.AreEqual("9", resp); + + //start larger than end + resp = db.StringGetRange(key, -1, -3); + ClassicAssert.AreEqual("", resp); + + //2,-1 -> 2 9 + var negend = -1; + resp = db.StringGetRange(key, 2, negend); + ClassicAssert.AreEqual(value.Substring(2, 8), resp); + + //2,-3 -> 2 7 + negend = -3; + resp = db.StringGetRange(key, 2, negend); + ClassicAssert.AreEqual(value.Substring(2, 6), resp); + + //-5,-3 -> 5,7 + var negstart = -5; + resp = db.StringGetRange(key, negstart, negend); + ClassicAssert.AreEqual(value.Substring(5, 3), resp); + } + + [Test] + public void SetRangeTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string key = "setRangeKey"; + string value = "0123456789"; + string newValue = "ABCDE"; + + // new key, length 10, offset 0 -> 10 ("0123456789") + var resp = (string)db.StringSetRange(key, 0, value); + ClassicAssert.AreEqual("10", resp); + resp = db.StringGet(key); + ClassicAssert.AreEqual("0123456789", resp); + ClassicAssert.IsTrue(db.KeyDelete(key)); + + // new key, length 10, offset 5 -> 15 ("\0\0\0\0\00123456789") + resp = db.StringSetRange(key, 5, value); + ClassicAssert.AreEqual("15", resp); + resp = db.StringGet(key); + ClassicAssert.AreEqual("\0\0\0\0\00123456789", resp); + ClassicAssert.IsTrue(db.KeyDelete(key)); + + // new key, length 10, offset -1 -> RedisServerException ("ERR offset is out of range") + try + { + db.StringSetRange(key, -1, value); + Assert.Fail(); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_GENERIC_OFFSETOUTOFRANGE), ex.Message); + } + + // existing key, length 10, offset 0, value length 5 -> 10 ("ABCDE56789") + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = db.StringSetRange(key, 0, newValue); + ClassicAssert.AreEqual("10", resp); + resp = db.StringGet(key); + ClassicAssert.AreEqual("ABCDE56789", resp); + ClassicAssert.IsTrue(db.KeyDelete(key)); + + // existing key, length 10, offset 5, value length 5 -> 10 ("01234ABCDE") + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = db.StringSetRange(key, 5, newValue); + ClassicAssert.AreEqual("10", resp); + resp = db.StringGet(key); + ClassicAssert.AreEqual("01234ABCDE", resp); + ClassicAssert.IsTrue(db.KeyDelete(key)); + + // existing key, length 10, offset 10, value length 5 -> 15 ("0123456789ABCDE") + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = db.StringSetRange(key, 10, newValue); + ClassicAssert.AreEqual("15", resp); + resp = db.StringGet(key); + ClassicAssert.AreEqual("0123456789ABCDE", resp); + ClassicAssert.IsTrue(db.KeyDelete(key)); + + // existing key, length 10, offset 15, value length 5 -> 20 ("0123456789\0\0\0\0\0ABCDE") + ClassicAssert.IsTrue(db.StringSet(key, value)); + resp = db.StringSetRange(key, 15, newValue); + ClassicAssert.AreEqual("20", resp); + resp = db.StringGet(key); + ClassicAssert.AreEqual("0123456789\0\0\0\0\0ABCDE", resp); + ClassicAssert.IsTrue(db.KeyDelete(key)); + + // existing key, length 10, offset -1, value length 5 -> RedisServerException ("ERR offset is out of range") + ClassicAssert.IsTrue(db.StringSet(key, value)); + try + { + db.StringSetRange(key, -1, newValue); + Assert.Fail(); + } + catch (RedisServerException ex) + { + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_GENERIC_OFFSETOUTOFRANGE), ex.Message); + } + } + + [Test] + public void PingTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string result = (string)db.Execute("PING"); + ClassicAssert.AreEqual("PONG", result); + } + + [Test] + public void AskingTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string result = (string)db.Execute("ASKING"); + ClassicAssert.AreEqual("OK", result); + } + + [Test] + public void KeepTtlTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + int expire = 3; + var keyA = "keyA"; + var keyB = "keyB"; + db.StringSet(keyA, keyA); + db.StringSet(keyB, keyB); + + db.KeyExpire(keyA, TimeSpan.FromSeconds(expire)); + db.KeyExpire(keyB, TimeSpan.FromSeconds(expire)); + + db.StringSet(keyA, keyA, keepTtl: true); + var time = db.KeyTimeToLive(keyA); + ClassicAssert.IsTrue(time.Value.Ticks > 0); + + db.StringSet(keyB, keyB, keepTtl: false); + time = db.KeyTimeToLive(keyB); + ClassicAssert.IsTrue(time == null); + + Thread.Sleep(expire * 1000 + 100); + + string value = db.StringGet(keyA); + ClassicAssert.AreEqual(null, value); + + value = db.StringGet(keyB); + ClassicAssert.AreEqual(keyB, value); + } + + [Test] + public void StrlenTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + ClassicAssert.IsTrue(db.StringSet("mykey", "foo bar")); + ClassicAssert.IsTrue(db.StringLength("mykey") == 7); + ClassicAssert.IsTrue(db.StringLength("nokey") == 0); + } + + [Test] + public void TTLTestMilliseconds() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "myKey"; + var val = "myKeyValue"; + var expireTimeInMilliseconds = 3000; + + var pttl = db.Execute("PTTL", key); + ClassicAssert.AreEqual(-2, (int)pttl); + + db.StringSet(key, val); + pttl = db.Execute("PTTL", key); + ClassicAssert.AreEqual(-1, (int)pttl); + + db.KeyExpire(key, TimeSpan.FromMilliseconds(expireTimeInMilliseconds)); + + //check TTL of the key in milliseconds + pttl = db.Execute("PTTL", key); + + ClassicAssert.IsTrue(long.TryParse(pttl.ToString(), out var pttlInMs)); + ClassicAssert.IsTrue(pttlInMs > 0); + + db.KeyPersist(key); + Thread.Sleep(expireTimeInMilliseconds); + + var _val = db.StringGet(key); + ClassicAssert.AreEqual(val, _val.ToString()); + + var ttl = db.KeyTimeToLive(key); + ClassicAssert.IsNull(ttl); + } + + [Test] + public void GetDelTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "myKey"; + var val = "myKeyValue"; + + // Key Setup + db.StringSet(key, val); + var retval = db.StringGet(key); + ClassicAssert.AreEqual(val, retval.ToString()); + + retval = db.StringGetDelete(key); + ClassicAssert.AreEqual(val, retval.ToString()); + + // Try retrieving already deleted key + retval = db.StringGetDelete(key); + ClassicAssert.AreEqual(string.Empty, retval.ToString()); + + // Try retrieving & deleting non-existent key + retval = db.StringGetDelete("nonExistentKey"); + ClassicAssert.AreEqual(string.Empty, retval.ToString()); + + // Key setup with metadata + key = "myKeyWithMetadata"; + val = "myValueWithMetadata"; + db.StringSet(key, val, expiry: TimeSpan.FromSeconds(10000)); + retval = db.StringGet(key); + ClassicAssert.AreEqual(val, retval.ToString()); + + retval = db.StringGetDelete(key); + ClassicAssert.AreEqual(val, retval.ToString()); + + // Try retrieving already deleted key with metadata + retval = db.StringGetDelete(key); + ClassicAssert.AreEqual(string.Empty, retval.ToString()); + } + + [Test] + public void AppendTest() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "myKey"; + var val = "myKeyValue"; + var val2 = "myKeyValue2"; + + db.StringSet(key, val); + var len = db.StringAppend(key, val2); + ClassicAssert.AreEqual(val.Length + val2.Length, len); + + var _val = db.StringGet(key); + ClassicAssert.AreEqual(val + val2, _val.ToString()); + + // Test appending an empty string + db.StringSet(key, val); + var len1 = db.StringAppend(key, ""); + ClassicAssert.AreEqual(val.Length, len1); + + _val = db.StringGet(key); + ClassicAssert.AreEqual(val, _val.ToString()); + + // Test appending to a non-existent key + var nonExistentKey = "nonExistentKey"; + var len2 = db.StringAppend(nonExistentKey, val2); + ClassicAssert.AreEqual(val2.Length, len2); + + _val = db.StringGet(nonExistentKey); + ClassicAssert.AreEqual(val2, _val.ToString()); + + // Test appending to a key with a large value + var largeVal = new string('a', 1000000); + db.StringSet(key, largeVal); + var len3 = db.StringAppend(key, val2); + ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); + + // Test appending to a key with metadata + var keyWithMetadata = "keyWithMetadata"; + db.StringSet(keyWithMetadata, val, TimeSpan.FromSeconds(10000)); + var len4 = db.StringAppend(keyWithMetadata, val2); + ClassicAssert.AreEqual(val.Length + val2.Length, len4); + + _val = db.StringGet(keyWithMetadata); + ClassicAssert.AreEqual(val + val2, _val.ToString()); + + var time = db.KeyTimeToLive(keyWithMetadata); + ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + } + + + [Test] + public void AppendTestEtagsetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "myKey"; + var val = "myKeyValue"; + var val2 = "myKeyValue2"; + + db.Execute("SETWITHETAG", [key, val]); + + var len = db.StringAppend(key, val2); + ClassicAssert.AreEqual(val.Length + val2.Length, len); + + var _val = db.StringGet(key); + ClassicAssert.AreEqual(val + val2, _val.ToString()); + + // Test appending an empty string + db.KeyDelete(key); + db.Execute("SETWITHETAG", [key, val]); + var len1 = db.StringAppend(key, ""); + ClassicAssert.AreEqual(val.Length, len1); + + _val = db.StringGet(key); + ClassicAssert.AreEqual(val, _val.ToString()); + + // Test appending to a non-existent key + var nonExistentKey = "nonExistentKey"; + var len2 = db.StringAppend(nonExistentKey, val2); + ClassicAssert.AreEqual(val2.Length, len2); + + _val = db.StringGet(nonExistentKey); + ClassicAssert.AreEqual(val2, _val.ToString()); + + // Test appending to a key with a large value + var largeVal = new string('a', 1000000); + db.KeyDelete(key); + db.Execute("SETWITHETAG", [ key, largeVal ]); + var len3 = db.StringAppend(key, val2); + ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); + + // Test appending to a key with metadata + var keyWithMetadata = "keyWithMetadata"; + db.Execute("SETWITHETAG", [ key, val]); + db.KeyExpire(key, TimeSpan.FromSeconds(10000)); + + var len4 = db.StringAppend(keyWithMetadata, val2); + ClassicAssert.AreEqual(val.Length + val2.Length, len4); + + _val = db.StringGet(keyWithMetadata); + ClassicAssert.AreEqual(val + val2, _val.ToString()); + + var time = db.KeyTimeToLive(keyWithMetadata); + ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + } + + [Test] + public void HelloTest1() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + // Test "HELLO 2" + var result = db.Execute("HELLO", "2"); + + ClassicAssert.IsNotNull(result); + ClassicAssert.AreEqual(ResultType.Array, result.Resp2Type); + ClassicAssert.AreEqual(ResultType.Array, result.Resp3Type); + var resultDict = result.ToDictionary(); + ClassicAssert.IsNotNull(resultDict); + ClassicAssert.AreEqual(2, (int)resultDict["proto"]); + ClassicAssert.AreEqual("master", (string)resultDict["role"]); + + // Test "HELLO 3" + result = db.Execute("HELLO", "3"); + + ClassicAssert.IsNotNull(result); + ClassicAssert.AreEqual(ResultType.Array, result.Resp2Type); + ClassicAssert.AreEqual(ResultType.Map, result.Resp3Type); + resultDict = result.ToDictionary(); + ClassicAssert.IsNotNull(resultDict); + ClassicAssert.AreEqual(3, (int)resultDict["proto"]); + ClassicAssert.AreEqual("master", (string)resultDict["role"]); + } + + [Test] + public void AsyncTest1() + { + // Set up low-memory database + TearDown(); + TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true, DisableObjects: true); + server.Start(); + + string firstKey = null, firstValue = null, lastKey = null, lastValue = null; + + // Load the data so that it spills to disk + using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) + { + var db = redis.GetDatabase(0); + + int keyCount = 5; + int valLen = 256; + int keyLen = 8; + + List> data = []; + for (int i = 0; i < keyCount; i++) + { + lastKey = GetRandomString(keyLen); + lastValue = GetRandomString(valLen); + if (firstKey == null) + { + firstKey = lastKey; + firstValue = lastValue; + } + data.Add(new Tuple(lastKey, lastValue)); + var pair = data.Last(); + db.StringSet(pair.Item1, pair.Item2); + } + } + + // We use newline counting for HELLO response as the exact length can vary slightly across versions + using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Newlines); + + var expectedNewlineCount = 32; // 32 '\n' characters expected in response + var response = lightClientRequest.Execute($"hello 3", expectedNewlineCount); + ClassicAssert.IsTrue(response.Length is > 180 and < 190); + + // Switch to byte counting in response + lightClientRequest.countResponseType = CountResponseType.Bytes; + + // Turn on async + var expectedResponse = "+OK\r\n"; + response = lightClientRequest.Execute($"async on", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + + // Get in-memory data item + expectedResponse = $"${lastValue.Length}\r\n{lastValue}\r\n"; + response = lightClientRequest.Execute($"GET {lastKey}", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + + // Get disk data item with async on + expectedResponse = $"-ASYNC 0\r\n>3\r\n$5\r\nasync\r\n$1\r\n0\r\n${firstValue.Length}\r\n{firstValue}\r\n"; + response = lightClientRequest.Execute($"GET {firstKey}", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + + // Issue barrier command for async + expectedResponse = "+OK\r\n"; + response = lightClientRequest.Execute($"async barrier", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + + // Turn off async + expectedResponse = "+OK\r\n"; + response = lightClientRequest.Execute($"async off", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + + // Get disk data item with async off + expectedResponse = $"${firstValue.Length}\r\n{firstValue}\r\n"; + response = lightClientRequest.Execute($"GET {firstKey}", expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, response); + } + #endregion } } \ No newline at end of file diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index efa73b7003..219a3d9827 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -258,27 +258,6 @@ public void LargeSetGet() ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); } - [Test] - public void LargeSetGetForEtagSetData() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - const int length = (1 << 19) + 100; - var value = new byte[length]; - - for (int i = 0; i < length; i++) - value[i] = (byte)((byte)'a' + ((byte)i % 26)); - - long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", value]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); - - // Backwards compatability of data set with etag and plain GET call - var retvalue = (byte[])db.StringGet("mykey"); - - ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); - } - [Test] public void SetExpiry() { @@ -316,51 +295,6 @@ public void SetExpiry() ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); } - - [Test] - public void SetExpiryForEtagSetData() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string origValue = "abcdefghij"; - - // set with etag - long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", origValue]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); - - // Expire the key in few seconds from now - ClassicAssert.IsTrue( - db.KeyExpire("mykey", TimeSpan.FromSeconds(2)) - ); - - string retValue = db.StringGet("mykey"); - ClassicAssert.AreEqual(origValue, retValue, "Get() before expiration"); - - var actualDbSize = db.Execute("DBSIZE"); - ClassicAssert.AreEqual(1, (ulong)actualDbSize, "DBSIZE before expiration"); - - var actualKeys = db.Execute("KEYS", ["*"]); - ClassicAssert.AreEqual(1, ((RedisResult[])actualKeys).Length, "KEYS before expiration"); - - var actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN before expiration"); - - Thread.Sleep(2500); - - retValue = db.StringGet("mykey"); - ClassicAssert.AreEqual(null, retValue, "Get() after expiration"); - - actualDbSize = db.Execute("DBSIZE"); - ClassicAssert.AreEqual(0, (ulong)actualDbSize, "DBSIZE after expiration"); - - actualKeys = db.Execute("KEYS", ["*"]); - ClassicAssert.AreEqual(0, ((RedisResult[])actualKeys).Length, "KEYS after expiration"); - - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); - } - [Test] public void SetExpiryHighPrecision() { @@ -497,76 +431,6 @@ public void SetGet() ClassicAssert.IsNull(expiry); } - [Test] - public void SetGetForEtagSetData() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string key = "mykey"; - string origValue = "abcdefghijklmnopqrst"; - - // Initial set - var _ = db.Execute("SETWITHETAG", [key, origValue]); - string retValue = db.StringGet(key); - ClassicAssert.AreEqual(origValue, retValue); - - // Smaller new value without expiration - string newValue1 = "abcdefghijklmnopqrs"; - retValue = db.StringSetAndGet(key, newValue1, null, When.Always, CommandFlags.None); - ClassicAssert.AreEqual(origValue, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue1, retValue); - - // HK TODO: This should increase the ETAG internally so add a check for that here - - // Smaller new value with KeepTtl - string newValue2 = "abcdefghijklmnopqr"; - retValue = db.StringSetAndGet(key, newValue2, null, true, When.Always, CommandFlags.None); - ClassicAssert.AreEqual(newValue1, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue2, retValue); - var expiry = db.KeyTimeToLive(key); - ClassicAssert.IsNull(expiry); - - // Smaller new value with expiration - string newValue3 = "01234"; - retValue = db.StringSetAndGet(key, newValue3, TimeSpan.FromSeconds(10), When.Exists, CommandFlags.None); - ClassicAssert.AreEqual(newValue2, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue3, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); - - // Larger new value with expiration - string newValue4 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - retValue = db.StringSetAndGet(key, newValue4, TimeSpan.FromSeconds(100), When.Exists, CommandFlags.None); - ClassicAssert.AreEqual(newValue3, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue4, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); - - // Smaller new value without expiration - string newValue5 = "0123401234"; - retValue = db.StringSetAndGet(key, newValue5, null, When.Exists, CommandFlags.None); - ClassicAssert.AreEqual(newValue4, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue5, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsNull(expiry); - - // Larger new value without expiration - string newValue6 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - retValue = db.StringSetAndGet(key, newValue6, null, When.Always, CommandFlags.None); - ClassicAssert.AreEqual(newValue5, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue6, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsNull(expiry); - } - - [Test] public void SetExpiryIncr() { diff --git a/test/Garnet.test/TransactionTests.cs b/test/Garnet.test/TransactionTests.cs index 76e91799b1..7df47511d4 100644 --- a/test/Garnet.test/TransactionTests.cs +++ b/test/Garnet.test/TransactionTests.cs @@ -304,6 +304,8 @@ public async Task WatchKeyFromDisk() ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); } + // HK TODO: Add test here for running etag cmds in a transaction + private static void updateKey(string key, string value) { using var lightClientRequest = TestUtils.CreateRequest(); From 2dbab859bc8465eaba60a38596a1c7356974cd6c Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 24 Sep 2024 16:31:23 +1000 Subject: [PATCH 03/87] complete test coverage and bugfixes --- libs/server/Resp/BasicCommands.cs | 71 +- .../Functions/MainStore/PrivateMethods.cs | 25 +- .../Storage/Functions/MainStore/RMWMethods.cs | 101 +- .../Functions/MainStore/VarLenInputMethods.cs | 24 +- .../ObjectStore/VarLenInputMethods.cs | 2 +- .../Storage/Session/MainStore/MainStoreOps.cs | 55 +- .../cs/src/core/Allocator/AllocatorScan.cs | 2 +- .../core/Allocator/SpanByteAllocatorImpl.cs | 2 +- .../ClientSession/SessionFunctionsWrapper.cs | 2 +- .../core/Compaction/LogCompactionFunctions.cs | 2 +- .../Index/Interfaces/ISessionFunctions.cs | 2 +- .../Index/Interfaces/SessionFunctionsBase.cs | 2 +- .../Implementation/InternalUpsert.cs | 3 + .../src/core/VarLen/IVariableLengthInput.cs | 2 +- .../cs/src/core/VarLen/SpanByteFunctions.cs | 2 +- test/Garnet.test/RespEtagTests.cs | 1323 ++++++----------- test/Garnet.test/RespTests.cs | 1 + test/Garnet.test/TransactionTests.cs | 2 - 18 files changed, 614 insertions(+), 1009 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 6b36a4041e..ac8d7c6196 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -281,7 +281,9 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); // Setup input buffer - SpanByte input = SpanByte.Reinterpret(stackalloc byte[NumUtils.MaximumFormatInt64Length]); + // (len of spanbyte + input header size) is our input buffer size + var inputSize = sizeof(int) + RespInputHeader.Size; + SpanByte input = SpanByte.Reinterpret(stackalloc byte[inputSize]); byte* inputPtr = input.ToPointer(); ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; ((RespInputHeader*)inputPtr)->flags = 0; @@ -327,7 +329,9 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); // Setup input buffer to pass command info, and the ETag to check with. - SpanByte input = SpanByte.Reinterpret(stackalloc byte[NumUtils.MaximumFormatInt64Length]); + // len + header + etag's data type size + var inputSize = RespInputHeader.Size + sizeof(long) + sizeof(int); + SpanByte input = SpanByte.Reinterpret(stackalloc byte[inputSize]); byte* inputPtr = input.ToPointer(); ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETIFNOTMATCH; ((RespInputHeader*)inputPtr)->flags = 0; @@ -371,22 +375,35 @@ private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) var key = parseState.GetArgSliceByRef(0).SpanByte; var value = parseState.GetArgSliceByRef(1).SpanByte; - var etagToCheckWith = parseState.GetLong(2); - - // Move value forward to make space for ETAG in Value itself - var initialValueSize = value.Length; - value.Length += sizeof(long); - var valPtr = value.ToPointer(); - Buffer.MemoryCopy(valPtr, sizeof(long) + valPtr, initialValueSize, initialValueSize); - // now insert the ETag at the start of the valPtr - *(long*)valPtr = etagToCheckWith; + // since the etag is a long, we now have a copy of it on the stack, and the underlying memory can be used as an extension for value's spanbyte later + long etagToCheckWith = parseState.GetLong(2); + + /* + Here we make space for etag to be added infront of value. We borrow 8 bytes from infront of the value, we will later restore the memory for the location we borrow. + P.s. This is NOT GOING TO create a buffer overflow becuase of the following reason. + Value spanbyte points to the network buffer, the network buffer is already holding key, value, and etag in a contiguous chunk of memory, in order, along with padding + for separators in Resp. This means there has to be ENOUGH OR MORE space for len(value) + sizeof(long). + So once we read the etag from the network buffer onto the stack, we can borrow 8 bytes of memory infront of value spanbyte safely, hence not creating a buffer overflow + when borrowing 8 bytes to shove the etag into the expanded spanbyte for value, which we then use as our input buffer. + */ + byte* borrowedMemLocation = value.ToPointer() + sizeof(long); + long saved8Bytes = *(long*)borrowedMemLocation; + int initialSizeOfValueSpan = value.Length; + value.Length = initialSizeOfValueSpan + sizeof(long); + // move contents of value 8 bytes forward + Buffer.MemoryCopy(value.ToPointer(), value.ToPointer() + sizeof(long), initialSizeOfValueSpan, initialSizeOfValueSpan); + // add the etag at first 8 bytes + *(long*)value.ToPointer() = etagToCheckWith; // Make space for key header var keyPtr = key.ToPointer() - sizeof(int); // Set key length *(int*)keyPtr = key.Length; - NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, valPtr - sizeof(int), value.Length, true, false, ref storageApi); + NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, ref storageApi); + + // restore the 8 bytes we had messed with on the network buffer + *(long*)borrowedMemLocation = saved8Bytes; return true; } @@ -404,24 +421,12 @@ private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) var key = parseState.GetArgSliceByRef(0).SpanByte; var value = parseState.GetArgSliceByRef(1).SpanByte; - // Move value forward to make space for ETAG in Value - var initialValueSize = value.Length; - value.Length += sizeof(long); - var valPtr = value.ToPointer(); - Buffer.MemoryCopy(valPtr, sizeof(long) + valPtr, initialValueSize, initialValueSize); - // now insert the ETag at the start of the valPtr - long initialEtag = 0; - *(long*)valPtr = initialEtag; - // Make space for key header var keyPtr = key.ToPointer() - sizeof(int); // Set key length *(int*)keyPtr = key.Length; - NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, valPtr - sizeof(int), value.Length, false, false, ref storageApi); - - while (!RespWriteUtils.WriteInteger(initialEtag, ref dcurr, dend)) - SendAndReset(); + NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, ref storageApi); return true; } @@ -831,7 +836,13 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); var status = storageApi.SET_Conditional(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(inputPtr), ref o, cmd); - + + // not found for a setwithetag is okay, and so we invert it + if (cmd == RespCommand.SETWITHETAG && status == GarnetStatus.NOTFOUND) + { + status = GarnetStatus.OK; + } + // Status tells us whether an old image was found during RMW or not switch (status) { @@ -864,17 +875,15 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt bool ok = status != GarnetStatus.NOTFOUND; // Status tells us whether an old image was found during RMW or not - // For a "set if not exists" or "set with etag", NOTFOUND means the operation succeeded + // For a "set if not exists" NOTFOUND means the operation succeeded // So we invert the ok flag - if (cmd == RespCommand.SETEXNX || cmd == RespCommand.SETWITHETAG) + if (cmd == RespCommand.SETEXNX) ok = !ok; if (!ok) { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); - } - // SETWITHETAG writes back the initial ETAG set back to client outside of this method - else if (cmd != RespCommand.SETWITHETAG) + } else { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index e66d9941f5..f418843a52 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -123,13 +123,13 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd // Get value without RESP header; exclude expiration if (value.LengthWithoutMetadata <= dst.Length) { - dst.Length = value.LengthWithoutMetadata; + dst.Length = value.LengthWithoutMetadata - payloadEtagEnd; value.AsReadOnlySpan(payloadEtagEnd).CopyTo(dst.SpanByte.AsSpan()); return; } dst.ConvertToHeap(); - dst.Length = value.LengthWithoutMetadata; + dst.Length = value.LengthWithoutMetadata - payloadEtagEnd; dst.Memory = functionsState.memoryPool.Rent(value.LengthWithoutMetadata); value.AsReadOnlySpan(payloadEtagEnd).CopyTo(dst.Memory.Memory.Span); break; @@ -247,8 +247,8 @@ void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long return; } - dst.Length = desiredLength; dst.ConvertToHeap(); + dst.Length = desiredLength; dst.Memory = functionsState.memoryPool.Rent(desiredLength); fixed (byte* ptr = dst.Memory.Memory.Span) { @@ -321,7 +321,7 @@ bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, ref SpanB } } - void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref SpanByte input, ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output, int etagIgnoredOffset) + void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref SpanByte input, ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output) { ObjectOutputHeader* o = (ObjectOutputHeader*)output.SpanByte.ToPointer(); if (expiryExists) @@ -329,16 +329,16 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp switch (optionType) { case ExpireOption.NX: - oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); break; case ExpireOption.XX: case ExpireOption.None: newValue.ExtraMetadata = input.ExtraMetadata; - oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); o->result1 = 1; break; case ExpireOption.GT: - oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); bool replace = input.ExtraMetadata < oldValue.ExtraMetadata; newValue.ExtraMetadata = replace ? oldValue.ExtraMetadata : input.ExtraMetadata; if (replace) @@ -347,7 +347,7 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp o->result1 = 1; break; case ExpireOption.LT: - oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); replace = input.ExtraMetadata > oldValue.ExtraMetadata; newValue.ExtraMetadata = replace ? oldValue.ExtraMetadata : input.ExtraMetadata; if (replace) @@ -364,13 +364,13 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, ref Sp case ExpireOption.NX: case ExpireOption.None: newValue.ExtraMetadata = input.ExtraMetadata; - oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); o->result1 = 1; break; case ExpireOption.XX: case ExpireOption.GT: case ExpireOption.LT: - oldValue.AsReadOnlySpan(etagIgnoredOffset).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); o->result1 = 0; break; } @@ -424,9 +424,10 @@ static bool InPlaceUpdateNumber(long val, ref SpanByte value, ref SpanByteAndMem static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, long input, int valueOffset) { - var debuggerCheck = value.ToPointer(); // Check if value contains a valid number - if (!IsValidNumber(value.LengthWithoutMetadata - valueOffset, value.ToPointer() + valueOffset, output.SpanByte.AsSpan(), out var val)) + int valLen = value.LengthWithoutMetadata - valueOffset; + byte* valPtr = value.ToPointer() + valueOffset; + if (!IsValidNumber(valLen, valPtr, output.SpanByte.AsSpan(), out var val)) return true; try diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 1e8b4cb0f5..8c200de33f 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -131,7 +131,6 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va var appendPtr = *(long*)(inputPtr + RespInputHeader.Size + sizeof(int)); var appendSpan = new Span((byte*)appendPtr, appendSize); appendSpan.CopyTo(value.AsSpan()); - // HK TODO: Test CopyValueLengthToOutput(ref value, ref output, 0); break; case RespCommand.INCRBY: @@ -152,9 +151,23 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va // If incrby is being made for initial update then it was not made with etag so the offset is sent as 0 CopyUpdateNumber(-decrBy, ref value, ref output, 0); break; + case RespCommand.SETWITHETAG: + recordInfo.SetHasETag(); + + // Copy input to value + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + sizeof(long)); + value.ExtraMetadata = input.ExtraMetadata; + + // initial etag set to 0, this is a counter based etag that is incremented on change + *(long*)value.ToPointer() = 0; + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); + + // Copy initial etag to output + CopyRespNumber(0, ref output); + break; default: value.UnmarkExtraMetadata(); - + recordInfo.ETag = false; if (*inputPtr >= CustomCommandManager.StartOffset) { var functions = functionsState.customCommands[*inputPtr - CustomCommandManager.StartOffset].functions; @@ -178,9 +191,6 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va break; } - if ((RespCommand)(*inputPtr) == RespCommand.SETWITHETAG) - recordInfo.SetHasETag(); - // Copy input to value value.ShrinkSerializedLength(input.Length - RespInputHeader.Size); value.ExtraMetadata = input.ExtraMetadata; @@ -247,7 +257,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span switch (cmd) { - // HK TODO figure out where this logic is? case RespCommand.SETEXNX: // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) @@ -255,7 +264,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - break; + return true; case RespCommand.SETIFMATCH: if (!recordInfo.ETag) @@ -319,7 +328,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: // Need CU if no space for new value - if (value.MetadataSize + input.Length - RespInputHeader.Size > value.Length) return false; + if (value.MetadataSize + input.Length - RespInputHeader.Size > value.Length - etagIgnoredOffset) return false; // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) @@ -330,21 +339,21 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - value.ShrinkSerializedLength(value.MetadataSize + input.Length - RespInputHeader.Size); + value.ShrinkSerializedLength(value.MetadataSize + input.Length - RespInputHeader.Size + etagIgnoredOffset); // Copy input to value input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - break; + return true; case RespCommand.PEXPIRE: case RespCommand.EXPIRE: + // doesn't update etag ExpireOption optionType = (ExpireOption)(*(inputPtr + RespInputHeader.Size)); bool expiryExists = (value.MetadataSize > 0); return EvaluateExpireInPlace(optionType, expiryExists, ref input, ref value, ref output); case RespCommand.PERSIST: - // HK TODO: NO clue about this one if (value.MetadataSize != 0) { rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); @@ -354,7 +363,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); output.SpanByte.AsSpan()[0] = 1; } - break; + // does not update etag + return true; case RespCommand.INCR: if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) @@ -400,7 +410,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_0, ref output); else CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_1, ref output); - return true; + break; case RespCommand.BITFIELD: i = inputPtr + RespInputHeader.Size; v = value.ToPointer() + etagIgnoredOffset; @@ -426,7 +436,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span case RespCommand.PFADD: i = inputPtr + RespInputHeader.Size; - v = value.ToPointer() + etagIgnoredOffset; + v = value.ToPointer(); if (!HyperLogLog.DefaultHLL.IsValidHYLL(v, value.Length)) { @@ -440,16 +450,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span var result = HyperLogLog.DefaultHLL.Update(i, v, value.Length, ref updated); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (!result) - return false; + if (result) + *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; - *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; - break; + // doesnt update etag because this doesnt work with etag data + return result; case RespCommand.PFMERGE: //srcHLL offset: [hll allocated size = 4 byte] + [hll data structure] //memcpy +4 (skip len size) byte* srcHLL = inputPtr + RespInputHeader.Size + sizeof(int); - byte* dstHLL = value.ToPointer() + etagIgnoredOffset; + byte* dstHLL = value.ToPointer(); if (!HyperLogLog.DefaultHLL.IsValidHYLL(dstHLL, value.Length)) { @@ -460,23 +470,21 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(value.Length + value.MetadataSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (!HyperLogLog.DefaultHLL.TryMerge(srcHLL, dstHLL, value.Length)) - return false; - - break; + // doesnt update etag because this doesnt work with etag data + return HyperLogLog.DefaultHLL.TryMerge(srcHLL, dstHLL, value.Length); case RespCommand.SETRANGE: var offset = *(int*)(inputPtr + RespInputHeader.Size); var newValueSize = *(int*)(inputPtr + RespInputHeader.Size + sizeof(int)); var newValuePtr = new Span((byte*)*(long*)(inputPtr + RespInputHeader.Size + sizeof(int) * 2), newValueSize); - if (newValueSize + offset > value.LengthWithoutMetadata) + if (newValueSize + offset > value.LengthWithoutMetadata - etagIgnoredOffset) return false; newValuePtr.CopyTo(value.AsSpan(etagIgnoredOffset).Slice(offset)); CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); - return true; + break; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. @@ -493,7 +501,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span if (appendSize == 0) { CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); - break; + return true; } return false; @@ -631,6 +639,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa var cmd = (RespCommand)(*inputPtr); + bool shouldUpdateEtag = true; int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; long oldEtag = -1; @@ -646,8 +655,10 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.SETIFMATCH: Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data."); - // update the etag + // this update is so the early call to send the resp command works, outside of the switch + // we are doing a double op of setting the etag to normalize etag update for other operations *(long*)(input.ToPointer() + RespInputHeader.Size) += 1; + // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan()); @@ -658,7 +669,8 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.SET: case RespCommand.SETEXXX: - Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length); + // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX + Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length -etagIgnoredOffset); // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) @@ -669,14 +681,14 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; - // HK TODO: SETEXXX for a SETWITHETAG value says newvalue does not have enoug bytes here + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(etagIgnoredOffset)); break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: Debug.Assert(oldValue.MetadataSize + input.Length - RespInputHeader.Size == newValue.Length); - + // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { @@ -692,12 +704,14 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.EXPIRE: case RespCommand.PEXPIRE: Debug.Assert(newValue.Length == oldValue.Length + input.MetadataSize); + shouldUpdateEtag = false; ExpireOption optionType = (ExpireOption)(*(inputPtr + RespInputHeader.Size)); bool expiryExists = oldValue.MetadataSize > 0; - EvaluateExpireCopyUpdate(optionType, expiryExists, ref input, ref oldValue, ref newValue, ref output, etagIgnoredOffset); + EvaluateExpireCopyUpdate(optionType, expiryExists, ref input, ref oldValue, ref newValue, ref output); break; case RespCommand.PERSIST: + shouldUpdateEtag = false; oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); if (oldValue.MetadataSize != 0) { @@ -762,27 +776,29 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa break; case RespCommand.PFADD: + // HYPERLOG doesnt work with non hyperlog key values bool updated = false; - byte* newValPtr = newValue.ToPointer() + etagIgnoredOffset; - byte* oldValPtr = oldValue.ToPointer() + etagIgnoredOffset; + byte* newValPtr = newValue.ToPointer(); + byte* oldValPtr = oldValue.ToPointer(); if (newValue.Length != oldValue.Length) - updated = HyperLogLog.DefaultHLL.CopyUpdate(inputPtr + RespInputHeader.Size, oldValPtr, newValPtr, newValue.Length - etagIgnoredOffset); + updated = HyperLogLog.DefaultHLL.CopyUpdate(inputPtr + RespInputHeader.Size, oldValPtr, newValPtr, newValue.Length); else { - Buffer.MemoryCopy(oldValPtr, newValPtr, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); - HyperLogLog.DefaultHLL.Update(inputPtr + RespInputHeader.Size, newValPtr, newValue.Length - etagIgnoredOffset, ref updated); + Buffer.MemoryCopy(oldValPtr, newValPtr, newValue.Length - etagIgnoredOffset, oldValue.Length); + HyperLogLog.DefaultHLL.Update(inputPtr + RespInputHeader.Size, newValPtr, newValue.Length, ref updated); } *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; break; case RespCommand.PFMERGE: + // HYPERLOG doesnt work with non hyperlog key values //srcA offset: [hll allocated size = 4 byte] + [hll data structure] //memcpy +4 (skip len size) byte* srcHLLPtr = inputPtr + RespInputHeader.Size + sizeof(int); // HLL merging from - byte* oldDstHLLPtr = oldValue.ToPointer() + etagIgnoredOffset; // original HLL merging to (too small to hold its data plus srcA) - byte* newDstHLLPtr = newValue.ToPointer() + etagIgnoredOffset; // new HLL merging to (large enough to hold srcA and srcB + byte* oldDstHLLPtr = oldValue.ToPointer(); // original HLL merging to (too small to hold its data plus srcA) + byte* newDstHLLPtr = newValue.ToPointer(); // new HLL merging to (large enough to hold srcA and srcB - HyperLogLog.DefaultHLL.CopyUpdateMerge(srcHLLPtr, oldDstHLLPtr, newDstHLLPtr, oldValue.Length -etagIgnoredOffset, newValue.Length - etagIgnoredOffset); + HyperLogLog.DefaultHLL.CopyUpdateMerge(srcHLLPtr, oldDstHLLPtr, newDstHLLPtr, oldValue.Length, newValue.Length); break; case RespCommand.SETRANGE: @@ -799,7 +815,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); rmwInfo.Action = RMWAction.ExpireAndStop; return false; @@ -812,7 +828,8 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa var appendSpan = new Span((byte*)appendPtr, appendSize); // Append the new value with the client input at the end of the old data - appendSpan.CopyTo(newValue.AsSpan(etagIgnoredOffset).Slice(oldValue.LengthWithoutMetadata)); + // the oldValue.LengthWithoutMetadata already contains the etag offset here + appendSpan.CopyTo(newValue.AsSpan().Slice(oldValue.LengthWithoutMetadata)); CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); break; @@ -847,7 +864,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); // increment the Etag transparently if in place update happened - if (recordInfo.ETag) + if (recordInfo.ETag && shouldUpdateEtag) { *(long*)newValue.ToPointer() = oldEtag + 1; } diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index a608158ff0..03e906944e 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -84,7 +84,9 @@ public int GetRMWInitialValueLength(ref SpanByte input) ndigits = NumUtils.NumDigitsInLong(next, ref fNeg); return sizeof(int) + ndigits + (fNeg ? 1 : 0); - + case RespCommand.SETWITHETAG: + // same space as SET but with 8 additional bytes for etag at the front of the payload + return sizeof(int) + input.Length - RespInputHeader.Size + sizeof(long); default: if (cmd >= 200) { @@ -103,13 +105,14 @@ public int GetRMWInitialValueLength(ref SpanByte input) } /// - public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) + public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool hasEtag) { if (input.Length > 0) { var inputspan = input.AsSpan(); var inputPtr = input.ToPointer(); var cmd = inputspan[0]; + int etagOffset = hasEtag ? 8 : 0; switch ((RespCommand)cmd) { case RespCommand.INCR: @@ -118,14 +121,14 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) var slicedInputData = inputspan.Slice(RespInputHeader.Size, datalen); // We don't need to TryParse here because InPlaceUpdater will raise an error before we reach this point - var curr = NumUtils.BytesToLong(t.AsSpan()); + var curr = NumUtils.BytesToLong(t.AsSpan(etagOffset)); var next = curr + NumUtils.BytesToLong(slicedInputData); var fNeg = false; var ndigits = NumUtils.NumDigitsInLong(next, ref fNeg); ndigits += fNeg ? 1 : 0; - return sizeof(int) + ndigits + t.MetadataSize; + return sizeof(int) + ndigits + t.MetadataSize + etagOffset; case RespCommand.DECR: case RespCommand.DECRBY: @@ -133,7 +136,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) slicedInputData = inputspan.Slice(RespInputHeader.Size, datalen); // We don't need to TryParse here because InPlaceUpdater will raise an error before we reach this point - curr = NumUtils.BytesToLong(t.AsSpan()); + curr = NumUtils.BytesToLong(t.AsSpan(etagOffset)); var decrBy = NumUtils.BytesToLong(slicedInputData); next = curr + (cmd == (byte)RespCommand.DECR ? decrBy : -decrBy); @@ -141,7 +144,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) ndigits = NumUtils.NumDigitsInLong(next, ref fNeg); ndigits += fNeg ? 1 : 0; - return sizeof(int) + ndigits + t.MetadataSize; + return sizeof(int) + ndigits + t.MetadataSize + etagOffset; case RespCommand.SETBIT: return sizeof(int) + BitmapManager.NewBlockAllocLength(inputPtr + RespInputHeader.Size, t.Length); case RespCommand.BITFIELD: @@ -162,11 +165,12 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: - return sizeof(int) + t.MetadataSize + input.Length - RespInputHeader.Size; + return sizeof(int) + t.MetadataSize + input.Length - RespInputHeader.Size + etagOffset; - case RespCommand.SETIFMATCH: case RespCommand.SET: case RespCommand.SETEXXX: + return sizeof(int) + input.Length - RespInputHeader.Size + etagOffset; + case RespCommand.SETIFMATCH: case RespCommand.PERSIST: break; @@ -178,8 +182,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) var offset = *((int*)(inputPtr + RespInputHeader.Size)); var newValueSize = *((int*)(inputPtr + RespInputHeader.Size + sizeof(int))); - if (newValueSize + offset > t.LengthWithoutMetadata) - return sizeof(int) + newValueSize + offset + t.MetadataSize; + if (newValueSize + offset > t.LengthWithoutMetadata - etagOffset) + return sizeof(int) + newValueSize + offset + t.MetadataSize + etagOffset; return sizeof(int) + t.Length; case RespCommand.GETDEL: diff --git a/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs index dff1d58284..9e27e299c5 100644 --- a/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs @@ -12,7 +12,7 @@ namespace Garnet.server public readonly unsafe partial struct ObjectSessionFunctions : ISessionFunctions { /// - public int GetRMWModifiedValueLength(ref IGarnetObject value, ref ObjectInput input) + public int GetRMWModifiedValueLength(ref IGarnetObject value, ref ObjectInput input, bool hasEtag) { throw new GarnetException("GetRMWModifiedValueLength is not available on the object store"); } diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index ee6a65b2a0..76179ce4c4 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System; -using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; using Garnet.common; @@ -647,6 +646,15 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S var expireSpan = new SpanByteAndMemory(); var ttlStatus = TTL(ref oldKey, storeType, ref expireSpan, ref context, ref objectContext, true); + // Find if this is ETag based key + SpanByte getWithEtagInput = SpanByte.Reinterpret(stackalloc byte[NumUtils.MaximumFormatInt64Length]); + byte* inputPtr = getWithEtagInput.ToPointer(); + ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; + ((RespInputHeader*)inputPtr)->flags = 0; + + var etagAndDataOutput = new SpanByteAndMemory(); + var getWithEtagStatus = GETForETagCmd(ref oldKey, ref getWithEtagInput, ref etagAndDataOutput, ref context); + if (ttlStatus == GarnetStatus.OK && !expireSpan.IsSpanByte) { using var expireMemoryHandle = expireSpan.Memory.Memory.Pin(); @@ -654,7 +662,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _); // If the key has an expiration, set the new key with the expiration - if (expireTimeMs > 0) + if (expireTimeMs > 0 && getWithEtagStatus == GarnetStatus.WRONGTYPE) { if (isNX) { @@ -677,7 +685,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context); } } - else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key + else if (expireTimeMs == -1 && getWithEtagStatus == GarnetStatus.WRONGTYPE) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key { if (isNX) { @@ -701,6 +709,47 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S SET(ref newKey, ref value, ref context); } } + else if ( + (expireTimeMs == -1 || expireTimeMs > 0) && + getWithEtagStatus == GarnetStatus.OK) + { + SpanByte newKey = newKeySlice.SpanByte; + + var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); + var initialValueSize = value.Length; + + var valPtr = value.ToPointer(); + + SpanByte key = newKeySlice.SpanByte; + + // Make space for key header + var keyPtr = key.ToPointer() - sizeof(int); + // Set key length + *(int*)keyPtr = key.Length; + + // Make space for resp input header + valPtr -= (RespInputHeader.Size + sizeof(int)); + if (expireTimeMs == -1) // no expiration provided + { + *(int*)valPtr = RespInputHeader.Size + value.Length; + ((RespInputHeader*)(valPtr + sizeof(int)))->cmd = RespCommand.SETWITHETAG; + ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; + } + else + { + // Move payload forward to make space for metadata + Buffer.MemoryCopy(valPtr + sizeof(int) + RespInputHeader.Size, + valPtr + sizeof(int) + sizeof(long) + RespInputHeader.Size, value.Length, value.Length); + *(int*)valPtr = sizeof(long) + RespInputHeader.Size + value.Length; + ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->cmd = RespCommand.SETWITHETAG; + ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->flags = 0; + + SpanByte.Reinterpret(inputPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; + } + + SET_Conditional(ref Unsafe.AsRef(keyPtr), + ref Unsafe.AsRef(valPtr), ref context); + } expireSpan.Memory.Dispose(); memoryHandle.Dispose(); diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs index 4ab5247f20..0372968117 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs @@ -334,7 +334,7 @@ public void PostInitialUpdater(ref TKey key, ref TInput input, ref TValue value, public void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput output, Empty ctx, Status status, RecordMetadata recordMetadata) { } - public int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => 0; + public int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag) => 0; public int GetRMWInitialValueLength(ref TInput input) => 0; public void ConvertOutputToHeap(ref TInput input, ref TOutput output) { } diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs index baa4bb1efa..4c3a5a57f2 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs @@ -127,7 +127,7 @@ public ref SpanByte GetAndInitializeValue(long physicalAddress, long endAddress) { // Used by RMW to determine the length of copy destination (taking Input into account), so does not need to get filler length. var keySize = key.TotalSize; - var size = RecordInfo.GetLength() + RoundUp(keySize, Constants.kRecordAlignment) + varlenInput.GetRMWModifiedValueLength(ref value, ref input); + var size = RecordInfo.GetLength() + RoundUp(keySize, Constants.kRecordAlignment) + varlenInput.GetRMWModifiedValueLength(ref value, ref input, recordInfo.ETag); return (size, RoundUp(size, Constants.kRecordAlignment), keySize); } diff --git a/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs b/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs index 86fbb050e8..671fcf4960 100644 --- a/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs +++ b/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs @@ -195,7 +195,7 @@ public void UnlockTransientShared(ref TKey key, ref OperationStackContext _clientSession.functions.GetRMWInitialValueLength(ref input); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetRMWModifiedValueLength(ref TValue t, ref TInput input) => _clientSession.functions.GetRMWModifiedValueLength(ref t, ref input); + public int GetRMWModifiedValueLength(ref TValue t, ref TInput input, bool hasEtag) => _clientSession.functions.GetRMWModifiedValueLength(ref t, ref input, hasEtag); [MethodImpl(MethodImplOptions.AggressiveInlining)] public IHeapContainer GetHeapContainer(ref TInput input) diff --git a/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs b/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs index 29cee3e636..182d142594 100644 --- a/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs @@ -50,7 +50,7 @@ public void ReadCompletionCallback(ref TKey key, ref TInput input, ref TOutput o public void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput output, TContext ctx, Status status, RecordMetadata recordMetadata) { } - public int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => 0; + public int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag) => 0; public int GetRMWInitialValueLength(ref TInput input) => 0; /// diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs index 2f778819cf..ff83a12f39 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs @@ -183,7 +183,7 @@ public interface ISessionFunctions /// /// Length of resulting value object when performing RMW modification of value using given input /// - int GetRMWModifiedValueLength(ref TValue value, ref TInput input); + int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag); /// /// Initial expected length of value object when populated by RMW using given input diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs index 496f234366..6da1b6a3b9 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs @@ -54,7 +54,7 @@ public virtual void ReadCompletionCallback(ref TKey key, ref TInput input, ref T public virtual void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput output, TContext ctx, Status status, RecordMetadata recordMetadata) { } /// - public virtual int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => throw new TsavoriteException("GetRMWModifiedValueLength is only available for SpanByte Functions"); + public virtual int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag) => throw new TsavoriteException("GetRMWModifiedValueLength is only available for SpanByte Functions"); /// public virtual int GetRMWInitialValueLength(ref TInput input) => throw new TsavoriteException("GetRMWInitialValueLength is only available for SpanByte Functions"); diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs index 9e69fd56c7..1da6263d3a 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs @@ -74,6 +74,8 @@ internal OperationStatus InternalUpsert= hlogBase.ReadOnlyAddress) { srcRecordInfo = ref stackCtx.recSrc.GetInfo(); + srcRecordInfo.ETag = false; // Mutable Region: Update the record in-place. We perform mutable updates only if we are in normal processing phase of checkpointing UpsertInfo upsertInfo = new() diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs index 9eaa027f39..37f5d9dc32 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs @@ -11,7 +11,7 @@ public interface IVariableLengthInput /// /// Length of resulting value object when performing RMW modification of value using given input /// - int GetRMWModifiedValueLength(ref TValue value, ref TInput input); + int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag); /// /// Initial expected length of value object when populated by RMW using given input diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs index 5350548625..6e699bcce7 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs @@ -120,7 +120,7 @@ public override bool InPlaceUpdater(ref SpanByte key, ref SpanByte input, ref Sp /// Length of resulting object when doing RMW with given value and input. Here we set the length /// to the max of input and old value lengths. You can provide a custom implementation for other cases. /// - public override int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) + public override int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool hasEtag) => sizeof(int) + (t.Length > input.Length ? t.Length : input.Length); /// diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index b1ddd2cb2a..3c1fefc488 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -2,11 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Authentication; using System.Text; using System.Threading; using System.Threading.Tasks; -using Garnet.common; using Garnet.server; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -25,7 +23,7 @@ public void Setup() { r = new Random(674386); TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true); + server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, disablePubSub: false); server.Start(); } @@ -59,6 +57,7 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() long initalEtag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, initalEtag); + // set a bigger val RedisResult[] setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", initalEtag]); long nextEtag = long.Parse(setIfMatchRes[0].ToString()); @@ -67,6 +66,7 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() ClassicAssert.AreEqual(1, nextEtag); ClassicAssert.AreEqual(value, "nextone"); + // set a bigger val setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextnextone", nextEtag]); nextEtag = long.Parse(setIfMatchRes[0].ToString()); value = setIfMatchRes[1].ToString(); @@ -74,6 +74,7 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() ClassicAssert.AreEqual(2, nextEtag); ClassicAssert.AreEqual(value, "nextnextone"); + // set a smaller val setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "lastOne", nextEtag]); nextEtag = long.Parse(setIfMatchRes[0].ToString()); value = setIfMatchRes[1].ToString(); @@ -177,6 +178,20 @@ public void GetIfNotMatchOnNonEtagDataReturnsWrongType() ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); } + [Test] + public void GetWithEtagOnNonEtagDataReturnsWrongType() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var _ = db.StringSet("h", "k"); + + RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", ["h"])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + } + #endregion #region Backwards Compatability Testing @@ -210,7 +225,7 @@ public async Task SingleUnicodeEtagSetGetGarnetClient() } [Test] - public void LargeEtagSetGet() + public async Task LargeEtagSetGet() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -221,11 +236,12 @@ public void LargeEtagSetGet() for (int i = 0; i < length; i++) value[i] = (byte)((byte)'a' + ((byte)i % 26)); - long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", value]).ToString()); + RedisResult res = await db.ExecuteAsync("SETWITHETAG", ["mykey", value]); + long initalEtag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, initalEtag); // Backwards compatability of data set with etag and plain GET call - var retvalue = (byte[])db.StringGet("mykey"); + var retvalue = (byte[]) await db.StringGetAsync("mykey"); ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); } @@ -300,8 +316,6 @@ public void SetExpiryHighPrecisionForEtagSetDatat() ClassicAssert.AreEqual(null, retValue); } - // HK TODO: Keep working from here - [Test] public void SetGetForEtagSetData() { @@ -330,43 +344,45 @@ public void SetGetForEtagSetData() // Smaller new value with KeepTtl string newValue2 = "abcdefghijklmnopqr"; retValue = db.StringSetAndGet(key, newValue2, null, true, When.Always, CommandFlags.None); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(2, checkEtag); + ClassicAssert.AreEqual(newValue1, retValue); retValue = db.StringGet(key); ClassicAssert.AreEqual(newValue2, retValue); var expiry = db.KeyTimeToLive(key); ClassicAssert.IsNull(expiry); - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); - ClassicAssert.AreEqual(2, checkEtag); - // Smaller new value with expiration string newValue3 = "01234"; retValue = db.StringSetAndGet(key, newValue3, TimeSpan.FromSeconds(10), When.Exists, CommandFlags.None); ClassicAssert.AreEqual(newValue2, retValue); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(3, checkEtag); + retValue = db.StringGet(key); ClassicAssert.AreEqual(newValue3, retValue); expiry = db.KeyTimeToLive(key); ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); - ClassicAssert.AreEqual(3, checkEtag); - // Larger new value with expiration string newValue4 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - // HK TODO: WHY RETURNING NULL!? retValue = db.StringSetAndGet(key, newValue4, TimeSpan.FromSeconds(100), When.Exists, CommandFlags.None); ClassicAssert.AreEqual(newValue3, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue4, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); // This should increase the ETAG internally so we have a check for that here checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); ClassicAssert.AreEqual(4, checkEtag); + retValue = db.StringGet(key); + ClassicAssert.AreEqual(newValue4, retValue); + expiry = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); + // Smaller new value without expiration string newValue5 = "0123401234"; retValue = db.StringSetAndGet(key, newValue5, null, When.Exists, CommandFlags.None); @@ -377,8 +393,8 @@ public void SetGetForEtagSetData() ClassicAssert.IsNull(expiry); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ]).ToString()); - ClassicAssert.AreEqual(4, checkEtag); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(5, checkEtag); // Larger new value without expiration string newValue6 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; @@ -390,13 +406,12 @@ public void SetGetForEtagSetData() ClassicAssert.IsNull(expiry); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ]).ToString()); - ClassicAssert.AreEqual(5, checkEtag); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + ClassicAssert.AreEqual(6, checkEtag); } - [Test] - public void SetExpiryIncr() + public void SetExpiryIncrForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -404,25 +419,46 @@ public void SetExpiryIncr() // Key storing integer var nVal = -100000; var strKey = "key1"; - db.StringSet(strKey, nVal, TimeSpan.FromSeconds(1)); + db.Execute("SETWITHETAG", [strKey, nVal]); + db.KeyExpire(strKey, TimeSpan.FromSeconds(5)); + + string res1 = db.StringGet(strKey); long n = db.StringIncrement(strKey); - long nRetVal = Convert.ToInt64(db.StringGet(strKey)); + + // This should increase the ETAG internally so we have a check for that here + var checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(1, checkEtag); + + string res = db.StringGet(strKey); + long nRetVal = Convert.ToInt64(res); ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(-99999, nRetVal); n = db.StringIncrement(strKey); + + // This should increase the ETAG internally so we have a check for that here + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(2, checkEtag); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(-99998, nRetVal); Thread.Sleep(5000); - // Expired key, restart increment + // Expired key, restart increment,after exp this is treated as new record + // without etag n = db.StringIncrement(strKey); + ClassicAssert.AreEqual(1, n); + nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(1, nRetVal); + + RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [ strKey ])); + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); } [Test] @@ -431,158 +467,77 @@ public void IncrDecrChangeDigitsWithExpiry() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - // Key storing integer var strKey = "key1"; - db.StringSet(strKey, 9, TimeSpan.FromSeconds(1000)); + + db.Execute("SETWITHETAG", [strKey, 9]); + + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(0, checkEtag); + + db.KeyExpire(strKey, TimeSpan.FromSeconds(5)); long n = db.StringIncrement(strKey); long nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(10, nRetVal); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(1, checkEtag); + n = db.StringDecrement(strKey); nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(9, nRetVal); - db.StringSet(strKey, 99, TimeSpan.FromSeconds(1000)); - n = db.StringIncrement(strKey); - nRetVal = Convert.ToInt64(db.StringGet(strKey)); - ClassicAssert.AreEqual(n, nRetVal); - ClassicAssert.AreEqual(100, nRetVal); - - n = db.StringDecrement(strKey); - nRetVal = Convert.ToInt64(db.StringGet(strKey)); - ClassicAssert.AreEqual(n, nRetVal); - ClassicAssert.AreEqual(99, nRetVal); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(2, checkEtag); - db.StringSet(strKey, 999, TimeSpan.FromSeconds(1000)); - n = db.StringIncrement(strKey); - nRetVal = Convert.ToInt64(db.StringGet(strKey)); - ClassicAssert.AreEqual(n, nRetVal); - ClassicAssert.AreEqual(1000, nRetVal); + Thread.Sleep(TimeSpan.FromSeconds(5)); - n = db.StringDecrement(strKey); - nRetVal = Convert.ToInt64(db.StringGet(strKey)); - ClassicAssert.AreEqual(n, nRetVal); - ClassicAssert.AreEqual(999, nRetVal); + var res = (string)db.StringGet(strKey); + ClassicAssert.IsNull(res); } [Test] - public void SetOptionsCaseSensitivityTest() + public void StringSetOnAnExistingEtagDataOverrides() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - var key = "csKey"; - var value = 1; - var setCommand = "SET"; - var ttlCommand = "TTL"; - var okResponse = "OK"; - - // xx - var resp = (string)db.Execute($"{setCommand}", key, value, "xx"); - ClassicAssert.IsNull(resp); - - ClassicAssert.IsTrue(db.StringSet(key, value)); - - // nx - resp = (string)db.Execute($"{setCommand}", key, value, "nx"); - ClassicAssert.IsNull(resp); - - // ex - resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1"); - ClassicAssert.AreEqual(okResponse, resp); - Thread.Sleep(TimeSpan.FromSeconds(1.1)); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out var ttl)); - ClassicAssert.AreEqual(-2, ttl); - - // px - resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000"); - ClassicAssert.AreEqual(okResponse, resp); - Thread.Sleep(TimeSpan.FromSeconds(1.1)); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); - ClassicAssert.AreEqual(-2, ttl); - - // keepttl - ClassicAssert.IsTrue(db.StringSet(key, 1, TimeSpan.FromMinutes(1))); - resp = (string)db.Execute($"{setCommand}", key, value, "keepttl"); - ClassicAssert.AreEqual(okResponse, resp); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out ttl) && ttl > 0 && ttl < 60); - - // ex .. nx, non-existing key - ClassicAssert.IsTrue(db.KeyDelete(key)); - resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "nx"); - ClassicAssert.AreEqual(okResponse, resp); - Thread.Sleep(TimeSpan.FromSeconds(1.1)); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); - ClassicAssert.AreEqual(-2, ttl); - - // ex .. nx, existing key - ClassicAssert.IsTrue(db.StringSet(key, value)); - resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "nx"); - ClassicAssert.IsNull(resp); - - // ex .. xx, non-existing key - ClassicAssert.IsTrue(db.KeyDelete(key)); - resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "xx"); - ClassicAssert.IsNull(resp); - - // ex .. xx, existing key - ClassicAssert.IsTrue(db.StringSet(key, value)); - resp = (string)db.Execute($"{setCommand}", key, value, "ex", "1", "xx"); - ClassicAssert.AreEqual(okResponse, resp); - Thread.Sleep(TimeSpan.FromSeconds(1.1)); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); - ClassicAssert.AreEqual(-2, ttl); - - // px .. nx, non-existing key - ClassicAssert.IsTrue(db.KeyDelete(key)); - resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "nx"); - ClassicAssert.AreEqual(okResponse, resp); - Thread.Sleep(TimeSpan.FromSeconds(1.1)); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); - ClassicAssert.AreEqual(-2, ttl); - - // px .. nx, existing key - ClassicAssert.IsTrue(db.StringSet(key, value)); - resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "nx"); - ClassicAssert.IsNull(resp); - - // px .. xx, non-existing key - ClassicAssert.IsTrue(db.KeyDelete(key)); - resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "xx"); - ClassicAssert.IsNull(resp); - - // px .. xx, existing key - ClassicAssert.IsTrue(db.StringSet(key, value)); - resp = (string)db.Execute($"{setCommand}", key, value, "px", "1000", "xx"); - ClassicAssert.AreEqual(okResponse, resp); - Thread.Sleep(TimeSpan.FromSeconds(1.1)); - resp = (string)db.Execute($"{ttlCommand}", key); - ClassicAssert.IsTrue(int.TryParse(resp, out ttl)); - ClassicAssert.AreEqual(-2, ttl); + var strKey = "mykey"; + db.Execute("SETWITHETAG", [strKey, 9]); + + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(0, checkEtag); + + // override the setwithetag to a new value altogether, this will make it lose it's etag capability + // This is a limitation for Etags because plain sets are upserts (blind updates), and currently we + // cannot increase the latency in the common path for set to check beyong Readonly address for the + // existence of a record with ETag. This means that sets are complete upserts and clients need to use + // setifmatch if they want each consequent set to maintain the key value pair's etag property + ClassicAssert.IsTrue(db.StringSet(strKey, "ciaociao")); + + string retVal = db.StringGet(strKey).ToString(); + ClassicAssert.AreEqual("ciaociao", retVal); + + RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [ strKey ])); + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); } [Test] - public void LockTakeRelease() + public void LockTakeReleaseOnAValueInitiallySetWithEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); string key = "lock-key"; string value = "lock-value"; + + var initalEtag = long.Parse(db.Execute("SETWITHETAG", [key, value]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); var success = db.LockTake(key, value, TimeSpan.FromSeconds(100)); - ClassicAssert.IsTrue(success); - - success = db.LockTake(key, value, TimeSpan.FromSeconds(100)); ClassicAssert.IsFalse(success); success = db.LockRelease(key, value); @@ -609,78 +564,25 @@ public void LockTakeRelease() ClassicAssert.IsTrue(success); } - [Test] - [TestCase(10)] - [TestCase(50)] - [TestCase(100)] - public void SingleIncr(int bytesPerSend) - { - using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); - - // Key storing integer - var nVal = -100000; - var strKey = "key1"; - - var expectedResponse = "+OK\r\n"; - var response = lightClientRequest.Execute($"SET {strKey} {nVal}", expectedResponse.Length, bytesPerSend); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = "$7\r\n-100000\r\n"; - response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesPerSend); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = ":-99999\r\n"; - response = lightClientRequest.Execute($"INCR {strKey}", expectedResponse.Length, bytesPerSend); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = "$6\r\n-99999\r\n"; - response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesPerSend); - ClassicAssert.AreEqual(expectedResponse, response); - } - - [Test] - [TestCase(9999, 10)] - [TestCase(9999, 50)] - [TestCase(9999, 100)] - public void SingleIncrBy(long nIncr, int bytesSent) - { - using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); - - // Key storing integer - var nVal = 1000; - var strKey = "key1"; - - var expectedResponse = "+OK\r\n"; - var response = lightClientRequest.Execute($"SET {strKey} {nVal}", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = "$4\r\n1000\r\n"; - response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = $":{nIncr + nVal}\r\n"; - response = lightClientRequest.Execute($"INCRBY {strKey} {nIncr}", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = $"${(nIncr + nVal).ToString().Length}\r\n{nIncr + nVal}\r\n"; - response = lightClientRequest.Execute($"GET {strKey}", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - } - [Test] [TestCase("key1", 1000)] [TestCase("key1", 0)] - public void SingleDecr(string strKey, int nVal) + public void SingleDecrForEtagSetData(string strKey, int nVal) { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); // Key storing integer - db.StringSet(strKey, nVal); + var initalEtag = long.Parse(db.Execute("SETWITHETAG", [strKey, nVal]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + long n = db.StringDecrement(strKey); ClassicAssert.AreEqual(nVal - 1, n); long nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); + + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(1, checkEtag); } [Test] @@ -688,67 +590,30 @@ public void SingleDecr(string strKey, int nVal) [TestCase(-1000, -9000)] [TestCase(-10000, 9000)] [TestCase(9000, 10000)] - public void SingleDecrBy(long nVal, long nDecr) + public void SingleDecrByForEtagSetData(long nVal, long nDecr) { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); // Key storing integer val var strKey = "key1"; - db.StringSet(strKey, nVal); + var initalEtag = long.Parse(db.Execute("SETWITHETAG", [strKey, nVal]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + long n = db.StringDecrement(strKey, nDecr); int nRetVal = Convert.ToInt32(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); - } - - [Test] - public void SingleDecrByNoKey() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - long decrBy = 1000; - - // Key storing integer - var strKey = "key1"; - db.StringDecrement(strKey, decrBy); - - var retValStr = db.StringGet(strKey).ToString(); - int retVal = Convert.ToInt32(retValStr); - - ClassicAssert.AreEqual(-decrBy, retVal); - } - - [Test] - public void SingleIncrNoKey() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - // Key storing integer - var strKey = "key1"; - db.StringIncrement(strKey); - - int retVal = Convert.ToInt32(db.StringGet(strKey)); - - ClassicAssert.AreEqual(1, retVal); - - // Key storing integer - strKey = "key2"; - db.StringDecrement(strKey); - retVal = Convert.ToInt32(db.StringGet(strKey)); - - ClassicAssert.AreEqual(-1, retVal); + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + ClassicAssert.AreEqual(1, checkEtag); } [Test] - [TestCase(RespCommand.INCR, true)] - [TestCase(RespCommand.DECR, true)] - [TestCase(RespCommand.INCRBY, true)] - [TestCase(RespCommand.DECRBY, true)] - [TestCase(RespCommand.INCRBY, false)] - [TestCase(RespCommand.DECRBY, false)] - public void SimpleIncrementInvalidValue(RespCommand cmd, bool initialize) + [TestCase(RespCommand.INCR)] + [TestCase(RespCommand.DECR)] + [TestCase(RespCommand.INCRBY)] + [TestCase(RespCommand.DECRBY)] + public void SimpleIncrementInvalidValueForEtagSetdata(RespCommand cmd) { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -758,19 +623,17 @@ public void SimpleIncrementInvalidValue(RespCommand cmd, bool initialize) { var key = $"key{i}"; var exception = false; - if (initialize) - { - var resp = db.StringSet(key, values[i]); - ClassicAssert.AreEqual(true, resp); - } + var initalEtag = long.Parse(db.Execute("SETWITHETAG", [key, values[i]]).ToString()); + ClassicAssert.AreEqual(0, initalEtag); + try { _ = cmd switch { RespCommand.INCR => db.StringIncrement(key), RespCommand.DECR => db.StringDecrement(key), - RespCommand.INCRBY => initialize ? db.StringIncrement(key, 10L) : (long)db.Execute("INCRBY", [key, values[i]]), - RespCommand.DECRBY => initialize ? db.StringDecrement(key, 10L) : (long)db.Execute("DECRBY", [key, values[i]]), + RespCommand.INCRBY => db.StringIncrement(key, 10L), + RespCommand.DECRBY => db.StringDecrement(key, 10L), _ => throw new Exception($"Command {cmd} not supported!"), }; } @@ -789,29 +652,32 @@ public void SimpleIncrementInvalidValue(RespCommand cmd, bool initialize) [TestCase(RespCommand.DECR)] [TestCase(RespCommand.INCRBY)] [TestCase(RespCommand.DECRBY)] - public void SimpleIncrementOverflow(RespCommand cmd) + public void SimpleIncrementOverflowForEtagSetData(RespCommand cmd) { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); var exception = false; - var key = "test"; + var key = "test"; + try { switch (cmd) { case RespCommand.INCR: - _ = db.StringSet(key, long.MaxValue.ToString()); + _ = db.Execute("SETWITHETAG", [key, long.MaxValue.ToString()]); _ = db.StringIncrement(key); break; case RespCommand.DECR: - _ = db.StringSet(key, long.MinValue.ToString()); + _ = db.Execute("SETWITHETAG", [key, long.MinValue.ToString()]); _ = db.StringDecrement(key); break; case RespCommand.INCRBY: + _ = db.Execute("SETWITHETAG", [key, 0]); _ = db.Execute("INCRBY", [key, ulong.MaxValue.ToString()]); break; case RespCommand.DECRBY: + _ = db.Execute("SETWITHETAG", [key, 0]); _ = db.Execute("DECRBY", [key, ulong.MaxValue.ToString()]); break; } @@ -824,9 +690,10 @@ public void SimpleIncrementOverflow(RespCommand cmd) } ClassicAssert.IsTrue(exception); } + [Test] - public void SingleDelete() + public void SingleDeleteForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -834,14 +701,14 @@ public void SingleDelete() // Key storing integer var nVal = 100; var strKey = "key1"; - db.StringSet(strKey, nVal); + db.Execute("SETWITHETAG", [strKey, nVal]); db.KeyDelete(strKey); var retVal = Convert.ToBoolean(db.StringGet(strKey)); ClassicAssert.AreEqual(retVal, false); } [Test] - public void SingleDeleteWithObjectStoreDisabled() + public void SingleDeleteWithObjectStoreDisabledForEtagSetData() { TearDown(); @@ -853,7 +720,8 @@ public void SingleDeleteWithObjectStoreDisabled() var value = "1234"; using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - db.StringSet(key, value); + + db.Execute("SETWITHETAG", [key, value]); var resp = (string)db.StringGet(key); ClassicAssert.AreEqual(resp, value); @@ -873,7 +741,7 @@ private string GetRandomString(int len) } [Test] - public void SingleDeleteWithObjectStoreDisable_LTM() + public void SingleDeleteWithObjectStoreDisable_LTMForEtagSetData() { TearDown(); @@ -892,7 +760,7 @@ public void SingleDeleteWithObjectStoreDisable_LTM() { data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); var pair = data.Last(); - db.StringSet(pair.Item1, pair.Item2); + db.Execute("SETWITHETAG", [pair.Item1, pair.Item2]); } @@ -913,7 +781,7 @@ public void SingleDeleteWithObjectStoreDisable_LTM() } [Test] - public void MultiKeyDelete([Values] bool withoutObjectStore) + public void MultiKeyDeleteForEtagSetData([Values] bool withoutObjectStore) { if (withoutObjectStore) { @@ -935,7 +803,7 @@ public void MultiKeyDelete([Values] bool withoutObjectStore) { data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); var pair = data.Last(); - db.StringSet(pair.Item1, pair.Item2); + db.Execute("SETWITHETAG", [pair.Item1, pair.Item2]); } var keys = data.Select(x => (RedisKey)x.Item1).ToArray(); @@ -948,40 +816,7 @@ public void MultiKeyDelete([Values] bool withoutObjectStore) } [Test] - public void MultiKeyDeleteObjectStore() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - int keyCount = 10; - int setCount = 3; - int valLen = 16; - int keyLen = 8; - - List keys = []; - for (int i = 0; i < keyCount; i++) - { - keys.Add(GetRandomString(keyLen)); - var key = keys.Last(); - - for (int j = 0; j < setCount; j++) - { - var member = GetRandomString(valLen); - db.SetAdd(key, member); - } - } - - var redisKeys = keys.Select(x => (RedisKey)x).ToArray(); - var keysDeleted = db.KeyDeleteAsync(redisKeys); - keysDeleted.Wait(); - ClassicAssert.AreEqual(keysDeleted.Result, 10); - - var keysDel = db.KeyDelete(redisKeys); - ClassicAssert.AreEqual(keysDel, 0); - } - - [Test] - public void MultiKeyUnlink([Values] bool withoutObjectStore) + public void MultiKeyUnlinkForEtagSetData([Values] bool withoutObjectStore) { if (withoutObjectStore) { @@ -1003,7 +838,7 @@ public void MultiKeyUnlink([Values] bool withoutObjectStore) { data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); var pair = data.Last(); - db.StringSet(pair.Item1, pair.Item2); + db.Execute("SETWITHETAG", [pair.Item1, pair.Item2]); } var keys = data.Select(x => (object)x.Item1).ToArray(); @@ -1015,39 +850,7 @@ public void MultiKeyUnlink([Values] bool withoutObjectStore) } [Test] - public void MultiKeyUnlinkObjectStore() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - int keyCount = 10; - int setCount = 3; - int valLen = 16; - int keyLen = 8; - - List keys = []; - for (int i = 0; i < keyCount; i++) - { - keys.Add(GetRandomString(keyLen)); - var key = keys.Last(); - - for (int j = 0; j < setCount; j++) - { - var member = GetRandomString(valLen); - db.SetAdd(key, member); - } - } - - var redisKey = keys.Select(x => (object)x).ToArray(); - var keysDeleted = (string)db.Execute("unlink", redisKey); - ClassicAssert.AreEqual(Int32.Parse(keysDeleted), 10); - - keysDeleted = (string)db.Execute("unlink", redisKey); - ClassicAssert.AreEqual(Int32.Parse(keysDeleted), 0); - } - - [Test] - public void SingleExists([Values] bool withoutObjectStore) + public void SingleExistsForEtagSetData([Values] bool withoutObjectStore) { if (withoutObjectStore) { @@ -1063,7 +866,8 @@ public void SingleExists([Values] bool withoutObjectStore) var nVal = 100; var strKey = "key1"; ClassicAssert.IsFalse(db.KeyExists(strKey)); - db.StringSet(strKey, nVal); + + db.Execute("SETWITHETAG", [strKey, nVal]); bool fExists = db.KeyExists("key1", CommandFlags.None); ClassicAssert.AreEqual(fExists, true); @@ -1072,23 +876,9 @@ public void SingleExists([Values] bool withoutObjectStore) ClassicAssert.AreEqual(fExists, false); } - [Test] - public void SingleExistsObject() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - var key = "key"; - ClassicAssert.IsFalse(db.KeyExists(key)); - - var listData = new RedisValue[] { "a", "b", "c", "d" }; - var count = db.ListLeftPush(key, listData); - ClassicAssert.AreEqual(4, count); - ClassicAssert.True(db.KeyExists(key)); - } [Test] - public void MultipleExistsKeysAndObjects() + public void MultipleExistsKeysAndObjectsAndEtagData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1101,31 +891,41 @@ public void MultipleExistsKeysAndObjects() db.StringSet("foo", "bar"); - var exists = db.KeyExists(["key", "listKey", "zset:test", "foo"]); - ClassicAssert.AreEqual(3, exists); - } + db.Execute("SETWITHETAG", ["rizz", "bar"]); + var exists = db.KeyExists(["key", "listKey", "zset:test", "foo", "rizz"]); + ClassicAssert.AreEqual(4, exists); + } [Test] - public void SingleRename() + public void SingleRenameEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); string origValue = "test1"; - db.StringSet("key1", origValue); + long etag = long.Parse(db.Execute("SETWITHETAG", ["key1", origValue]).ToString()); + ClassicAssert.AreEqual(0, etag); db.KeyRename("key1", "key2"); string retValue = db.StringGet("key2"); ClassicAssert.AreEqual(origValue, retValue); + // other key now gives no result + ClassicAssert.AreEqual("", db.Execute("GETWITHETAG", ["key1"]).ToString()); + + // new Key value pair created with older value, the etag is reset here back to 0 + var res = (RedisResult[])db.Execute("GETWITHETAG", ["key2"]); + ClassicAssert.AreEqual("0", res[0].ToString()); + ClassicAssert.AreEqual(origValue, res[1].ToString()); + origValue = db.StringGet("key1"); ClassicAssert.AreEqual(null, origValue); } [Test] - public void SingleRenameKeyEdgeCase([Values] bool withoutObjectStore) + public void SingleRenameKeyEdgeCaseEtagSetData([Values] bool withoutObjectStore) { if (withoutObjectStore) { @@ -1149,7 +949,8 @@ public void SingleRenameKeyEdgeCase([Values] bool withoutObjectStore) //2. Key rename oldKey.Equals(newKey) string origValue = "test1"; - db.StringSet("key1", origValue); + db.Execute("SETWITHETAG", ["key1", origValue]); + bool renameRes = db.KeyRename("key1", "key1"); ClassicAssert.IsTrue(renameRes); string retValue = db.StringGet("key1"); @@ -1157,158 +958,7 @@ public void SingleRenameKeyEdgeCase([Values] bool withoutObjectStore) } [Test] - public void SingleRenameObjectStore() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - var origList = new RedisValue[] { "a", "b", "c", "d" }; - var key1 = "lkey1"; - var count = db.ListRightPush(key1, origList); - ClassicAssert.AreEqual(4, count); - - var result = db.ListRange(key1); - ClassicAssert.AreEqual(origList, result); - - var key2 = "lkey2"; - var rb = db.KeyRename(key1, key2); - ClassicAssert.IsTrue(rb); - result = db.ListRange(key1); - ClassicAssert.AreEqual(Array.Empty(), result); - - result = db.ListRange(key2); - ClassicAssert.AreEqual(origList, result); - } - - [Test] - public void CanSelectCommand() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - var reply = db.Execute("SELECT", "0"); - ClassicAssert.IsTrue(reply.ToString() == "OK"); - Assert.Throws(() => db.Execute("SELECT", "1")); - - //select again the def db - db.Execute("SELECT", "0"); - } - - [Test] - public void CanSelectCommandLC() - { - using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); - - var expectedResponse = "-ERR invalid database index.\r\n+PONG\r\n"; - var response = lightClientRequest.Execute("SELECT 1", "PING", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); - } - - [Test] - [TestCase(10)] - [TestCase(50)] - [TestCase(100)] - public void CanDoCommandsInChunks(int bytesSent) - { - // SETEX - using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); - - var expectedResponse = "+OK\r\n"; - var response = lightClientRequest.Execute("SETEX mykey 1 abcdefghij", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // GET - expectedResponse = "$10\r\nabcdefghij\r\n"; - response = lightClientRequest.Execute("GET mykey", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - Thread.Sleep(2000); - - // GET - expectedResponse = "$-1\r\n"; - response = lightClientRequest.Execute("GET mykey", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // DECR - expectedResponse = "+OK\r\n"; - response = lightClientRequest.Execute("SET mykeydecr 1", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = ":0\r\n"; - response = lightClientRequest.Execute("DECR mykeydecr", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = "$1\r\n0\r\n"; - response = lightClientRequest.Execute("GET mykeydecr", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // DEL - expectedResponse = ":1\r\n"; - response = lightClientRequest.Execute("DEL mykeydecr", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = "$-1\r\n"; - response = lightClientRequest.Execute("GET mykeydecr", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // EXISTS - expectedResponse = ":0\r\n"; - response = lightClientRequest.Execute("EXISTS mykeydecr", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // SET - expectedResponse = "+OK\r\n"; - response = lightClientRequest.Execute("SET mykey 1", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // RENAME - expectedResponse = "+OK\r\n"; - response = lightClientRequest.Execute("RENAME mykey mynewkey", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - // GET - expectedResponse = "$1\r\n1\r\n"; - response = lightClientRequest.Execute("GET mynewkey", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - } - - - [Test] - [TestCase(10)] - [TestCase(50)] - [TestCase(100)] - public void CanSetGetCommandsChunks(int bytesSent) - { - using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Bytes); - var sb = new StringBuilder(); - - for (int i = 1; i <= 100; i++) - { - sb.Append($" mykey-{i} {i * 10}"); - } - - // MSET - var expectedResponse = "+OK\r\n"; - var response = lightClientRequest.Execute($"MSET{sb}", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - expectedResponse = ":100\r\n"; - response = lightClientRequest.Execute($"DBSIZE", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - - sb.Clear(); - for (int i = 1; i <= 100; i++) - { - sb.Append($" mykey-{i}"); - } - - // MGET - expectedResponse = "*100\r\n$2\r\n10\r\n$2\r\n20\r\n$2\r\n30\r\n$2\r\n40\r\n$2\r\n50\r\n$2\r\n60\r\n$2\r\n70\r\n$2\r\n80\r\n$2\r\n90\r\n$3\r\n100\r\n$3\r\n110\r\n$3\r\n120\r\n$3\r\n130\r\n$3\r\n140\r\n$3\r\n150\r\n$3\r\n160\r\n$3\r\n170\r\n$3\r\n180\r\n$3\r\n190\r\n$3\r\n200\r\n$3\r\n210\r\n$3\r\n220\r\n$3\r\n230\r\n$3\r\n240\r\n$3\r\n250\r\n$3\r\n260\r\n$3\r\n270\r\n$3\r\n280\r\n$3\r\n290\r\n$3\r\n300\r\n$3\r\n310\r\n$3\r\n320\r\n$3\r\n330\r\n$3\r\n340\r\n$3\r\n350\r\n$3\r\n360\r\n$3\r\n370\r\n$3\r\n380\r\n$3\r\n390\r\n$3\r\n400\r\n$3\r\n410\r\n$3\r\n420\r\n$3\r\n430\r\n$3\r\n440\r\n$3\r\n450\r\n$3\r\n460\r\n$3\r\n470\r\n$3\r\n480\r\n$3\r\n490\r\n$3\r\n500\r\n$3\r\n510\r\n$3\r\n520\r\n$3\r\n530\r\n$3\r\n540\r\n$3\r\n550\r\n$3\r\n560\r\n$3\r\n570\r\n$3\r\n580\r\n$3\r\n590\r\n$3\r\n600\r\n$3\r\n610\r\n$3\r\n620\r\n$3\r\n630\r\n$3\r\n640\r\n$3\r\n650\r\n$3\r\n660\r\n$3\r\n670\r\n$3\r\n680\r\n$3\r\n690\r\n$3\r\n700\r\n$3\r\n710\r\n$3\r\n720\r\n$3\r\n730\r\n$3\r\n740\r\n$3\r\n750\r\n$3\r\n760\r\n$3\r\n770\r\n$3\r\n780\r\n$3\r\n790\r\n$3\r\n800\r\n$3\r\n810\r\n$3\r\n820\r\n$3\r\n830\r\n$3\r\n840\r\n$3\r\n850\r\n$3\r\n860\r\n$3\r\n870\r\n$3\r\n880\r\n$3\r\n890\r\n$3\r\n900\r\n$3\r\n910\r\n$3\r\n920\r\n$3\r\n930\r\n$3\r\n940\r\n$3\r\n950\r\n$3\r\n960\r\n$3\r\n970\r\n$3\r\n980\r\n$3\r\n990\r\n$4\r\n1000\r\n"; - response = lightClientRequest.Execute($"MGET{sb}", expectedResponse.Length, bytesSent); - ClassicAssert.AreEqual(expectedResponse, response); - } - - [Test] - public void PersistTTLTest() + public void PersistTTLTestForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1320,17 +970,28 @@ public void PersistTTLTest() var ttl = db.Execute("TTL", key); ClassicAssert.AreEqual(-2, (int)ttl); - db.StringSet(key, val); + db.Execute("SETWITHETAG", [key, val]); ttl = db.Execute("TTL", key); ClassicAssert.AreEqual(-1, (int)ttl); db.KeyExpire(key, TimeSpan.FromSeconds(expire)); + var res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(val, res[1].ToString()); + var time = db.KeyTimeToLive(key); ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); db.KeyExpire(key, TimeSpan.FromSeconds(expire)); + res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(val, res[1].ToString()); + db.KeyPersist(key); + res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(val, res[1].ToString()); Thread.Sleep((expire + 1) * 1000); @@ -1339,40 +1000,22 @@ public void PersistTTLTest() time = db.KeyTimeToLive(key); ClassicAssert.IsNull(time); - } - - [Test] - public void ObjectTTLTest() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - var key = "expireKey"; - var expire = 2; - - var ttl = db.Execute("TTL", key); - ClassicAssert.AreEqual(-2, (int)ttl); - - db.SortedSetAdd(key, key, 1.0); - ttl = db.Execute("TTL", key); - ClassicAssert.AreEqual(-1, (int)ttl); - - db.KeyExpire(key, TimeSpan.FromSeconds(expire)); - var time = db.KeyTimeToLive(key); - ClassicAssert.IsNotNull(time); - ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(val, res[1].ToString()); } [Test] - public void PersistTest() + public void PersistTestForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); int expire = 100; var keyA = "keyA"; - db.StringSet(keyA, keyA); + db.Execute("SETWITHETAG", [keyA, keyA]); + var response = db.KeyPersist(keyA); ClassicAssert.IsFalse(response); @@ -1389,47 +1032,25 @@ public void PersistTest() var value = db.StringGet(keyA); ClassicAssert.AreEqual(value, keyA); + var res = (RedisResult[])db.Execute("GETWITHETAG", [keyA]); + ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(keyA, res[1].ToString()); + var noKey = "noKey"; response = db.KeyPersist(noKey); ClassicAssert.IsFalse(response); } - [Test] - public void PersistObjectTest() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - int expire = 100; - var keyA = "keyA"; - db.SortedSetAdd(keyA, [new SortedSetEntry("element", 1.0)]); - var response = db.KeyPersist(keyA); - ClassicAssert.IsFalse(response); - - db.KeyExpire(keyA, TimeSpan.FromSeconds(expire)); - var time = db.KeyTimeToLive(keyA); - ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); - - response = db.KeyPersist(keyA); - ClassicAssert.IsTrue(response); - - time = db.KeyTimeToLive(keyA); - ClassicAssert.IsTrue(time == null); - - var value = db.SortedSetScore(keyA, "element"); - ClassicAssert.AreEqual(1.0, value); - } - [Test] [TestCase("EXPIRE")] [TestCase("PEXPIRE")] - public void KeyExpireStringTest(string command) + public void KeyExpireStringTestForEtagSetData(string command) { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); var key = "keyA"; - db.StringSet(key, key); + db.Execute("SETWITHETAG", [key, key]); var value = db.StringGet(key); ClassicAssert.AreEqual(key, (string)value); @@ -1448,56 +1069,14 @@ public void KeyExpireStringTest(string command) [Test] [TestCase("EXPIRE")] [TestCase("PEXPIRE")] - public void KeyExpireObjectTest(string command) - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - var key = "keyA"; - db.SortedSetAdd(key, [new SortedSetEntry("element", 1.0)]); - - var value = db.SortedSetScore(key, "element"); - ClassicAssert.AreEqual(1.0, value, "Get Score before expiration"); - - var actualDbSize = db.Execute("DBSIZE"); - ClassicAssert.AreEqual(1, (ulong)actualDbSize, "DBSIZE before expiration"); - - var actualKeys = db.Execute("KEYS", ["*"]); - ClassicAssert.AreEqual(1, ((RedisResult[])actualKeys).Length, "KEYS before expiration"); - - var actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN before expiration"); - - var exp = db.KeyExpire(key, command.Equals("EXPIRE") ? TimeSpan.FromSeconds(1) : TimeSpan.FromMilliseconds(1000)); - ClassicAssert.IsTrue(exp); - - // Sleep to wait for expiration - Thread.Sleep(1500); - - value = db.SortedSetScore(key, "element"); - ClassicAssert.AreEqual(null, value, "Get Score after expiration"); - - actualDbSize = db.Execute("DBSIZE"); - ClassicAssert.AreEqual(0, (ulong)actualDbSize, "DBSIZE after expiration"); - - actualKeys = db.Execute("KEYS", ["*"]); - ClassicAssert.AreEqual(0, ((RedisResult[])actualKeys).Length, "KEYS after expiration"); - - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after expiration"); - } - - [Test] - [TestCase("EXPIRE")] - [TestCase("PEXPIRE")] - public void KeyExpireOptionsTest(string command) + public void KeyExpireOptionsTestForEtagSetData(string command) { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); var key = "keyA"; object[] args = [key, 1000, ""]; - db.StringSet(key, key); + db.Execute("SETWITHETAG", [key, key]); args[2] = "XX";// XX -- Set expiry only when the key has an existing expiry bool resp = (bool)db.Execute($"{command}", args); @@ -1552,92 +1131,7 @@ public void KeyExpireOptionsTest(string command) } [Test] - public async Task ReAddExpiredKey() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - const string key = "x:expire_trap"; - - // Set - { - db.KeyDelete(key); - db.SetAdd(key, "v1"); - - ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial add"); - ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial add"); - var actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial ADD"); - - db.KeyExpire(key, TimeSpan.FromSeconds(1)); - await Task.Delay(TimeSpan.FromSeconds(2)); - - ClassicAssert.IsFalse(db.KeyExists(key), $"KeyExists after expiration"); - ClassicAssert.AreEqual("0", db.Execute("EXISTS", key).ToString(), "EXISTS after ADD expiration"); - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after ADD expiration"); - - db.SetAdd(key, "v2"); - - ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial re-ADD"); - ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial re-ADD"); - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial re-ADD"); - } - // List - { - db.KeyDelete(key); - db.ListRightPush(key, "v1"); - - ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial RPUSH"); - ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial RPUSH"); - var actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial RPUSH"); - - db.KeyExpire(key, TimeSpan.FromSeconds(1)); - await Task.Delay(TimeSpan.FromSeconds(2)); - - ClassicAssert.IsFalse(db.KeyExists(key), $"KeyExists after expiration"); - ClassicAssert.AreEqual("0", db.Execute("EXISTS", key).ToString(), "EXISTS after RPUSH expiration"); - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after RPUSH expiration"); - - db.ListRightPush(key, "v2"); - - ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial re-RPUSH"); - ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial re-RPUSH"); - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial re-RPUSH"); - } - // Hash - { - db.KeyDelete(key); - db.HashSet(key, "f1", "v1"); - - ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial HSET"); - ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial HSET"); - var actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial HSET"); - - db.KeyExpire(key, TimeSpan.FromSeconds(1)); - await Task.Delay(TimeSpan.FromSeconds(2)); - - ClassicAssert.IsFalse(db.KeyExists(key), $"KeyExists after expiration"); - ClassicAssert.AreEqual("0", db.Execute("EXISTS", key).ToString(), "EXISTS after HSET expiration"); - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(0, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after HSET expiration"); - - db.HashSet(key, "f1", "v2"); - - ClassicAssert.IsTrue(db.KeyExists(key), $"KeyExists after initial re-HSET"); - ClassicAssert.AreEqual("1", db.Execute("EXISTS", key).ToString(), "EXISTS after initial re-HSET"); - actualScan = db.Execute("SCAN", "0"); - ClassicAssert.AreEqual(1, ((RedisValue[])((RedisResult[])actualScan!)[1]).Length, "SCAN after initial re-HSET"); - } - } - - [Test] - public void MainObjectKey() + public void MainObjectKeyForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var server = redis.GetServers()[0]; @@ -1645,8 +1139,8 @@ public void MainObjectKey() const string key = "test:1"; - // Do StringSet - ClassicAssert.IsTrue(db.StringSet(key, "v1")); + // Do SETIWTHETAG + ClassicAssert.AreEqual(0, long.Parse(db.Execute("SETWITHETAG", [key, "v1"]).ToString())); // Do SetAdd using the same key ClassicAssert.IsTrue(db.SetAdd(key, "v2")); @@ -1666,7 +1160,7 @@ public void MainObjectKey() } [Test] - public void GetSliceTest() + public void GetSliceTestForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1676,7 +1170,8 @@ public void GetSliceTest() var resp = (string)db.StringGetRange(key, 2, 10); ClassicAssert.AreEqual(string.Empty, resp); - ClassicAssert.AreEqual(true, db.StringSet(key, value)); + + ClassicAssert.AreEqual(0, long.Parse(db.Execute("SETWITHETAG", [key, value]).ToString())); //0,0 resp = (string)db.StringGetRange(key, 0, 0); @@ -1783,7 +1278,7 @@ public void GetSliceTest() } [Test] - public void SetRangeTest() + public void SetRangeTestForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1792,18 +1287,21 @@ public void SetRangeTest() string value = "0123456789"; string newValue = "ABCDE"; - // new key, length 10, offset 0 -> 10 ("0123456789") - var resp = (string)db.StringSetRange(key, 0, value); - ClassicAssert.AreEqual("10", resp); - resp = db.StringGet(key); - ClassicAssert.AreEqual("0123456789", resp); - ClassicAssert.IsTrue(db.KeyDelete(key)); + db.Execute("SETWITHETAG", [key, value]); + + var resp = db.StringGet(key); + ClassicAssert.AreEqual("0123456789", resp.ToString()); // new key, length 10, offset 5 -> 15 ("\0\0\0\0\00123456789") resp = db.StringSetRange(key, 5, value); - ClassicAssert.AreEqual("15", resp); + ClassicAssert.AreEqual("15", resp.ToString()); resp = db.StringGet(key); - ClassicAssert.AreEqual("\0\0\0\0\00123456789", resp); + ClassicAssert.AreEqual("012340123456789", resp.ToString()); + + // should update the etag internally + var updatedEtagRes = db.Execute("GETWITHETAG", key); + ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); + ClassicAssert.IsTrue(db.KeyDelete(key)); // new key, length 10, offset -1 -> RedisServerException ("ERR offset is out of range") @@ -1818,39 +1316,51 @@ public void SetRangeTest() } // existing key, length 10, offset 0, value length 5 -> 10 ("ABCDE56789") - ClassicAssert.IsTrue(db.StringSet(key, value)); + db.Execute("SETWITHETAG", [key, value]); + resp = db.StringSetRange(key, 0, newValue); - ClassicAssert.AreEqual("10", resp); + ClassicAssert.AreEqual("10", resp.ToString()); resp = db.StringGet(key); - ClassicAssert.AreEqual("ABCDE56789", resp); + ClassicAssert.AreEqual("ABCDE56789", resp.ToString()); + + // should update the etag internally + updatedEtagRes = db.Execute("GETWITHETAG", key); + ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); + ClassicAssert.IsTrue(db.KeyDelete(key)); - // existing key, length 10, offset 5, value length 5 -> 10 ("01234ABCDE") - ClassicAssert.IsTrue(db.StringSet(key, value)); + // key, length 10, offset 5, value length 5 -> 10 ("01234ABCDE") + db.Execute("SETWITHETAG", [key, value]); + resp = db.StringSetRange(key, 5, newValue); - ClassicAssert.AreEqual("10", resp); + ClassicAssert.AreEqual("10", resp.ToString()); + + updatedEtagRes = db.Execute("GETWITHETAG", key); + ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); + resp = db.StringGet(key); - ClassicAssert.AreEqual("01234ABCDE", resp); + ClassicAssert.AreEqual("01234ABCDE", resp.ToString()); ClassicAssert.IsTrue(db.KeyDelete(key)); // existing key, length 10, offset 10, value length 5 -> 15 ("0123456789ABCDE") - ClassicAssert.IsTrue(db.StringSet(key, value)); + db.Execute("SETWITHETAG", [key, value]); resp = db.StringSetRange(key, 10, newValue); - ClassicAssert.AreEqual("15", resp); + ClassicAssert.AreEqual("15", resp.ToString()); resp = db.StringGet(key); - ClassicAssert.AreEqual("0123456789ABCDE", resp); + ClassicAssert.AreEqual("0123456789ABCDE", resp.ToString()); ClassicAssert.IsTrue(db.KeyDelete(key)); // existing key, length 10, offset 15, value length 5 -> 20 ("0123456789\0\0\0\0\0ABCDE") - ClassicAssert.IsTrue(db.StringSet(key, value)); + db.Execute("SETWITHETAG", [key, value]); + resp = db.StringSetRange(key, 15, newValue); - ClassicAssert.AreEqual("20", resp); + ClassicAssert.AreEqual("20", resp.ToString()); resp = db.StringGet(key); - ClassicAssert.AreEqual("0123456789\0\0\0\0\0ABCDE", resp); + ClassicAssert.AreEqual("0123456789\0\0\0\0\0ABCDE", resp.ToString()); ClassicAssert.IsTrue(db.KeyDelete(key)); // existing key, length 10, offset -1, value length 5 -> RedisServerException ("ERR offset is out of range") - ClassicAssert.IsTrue(db.StringSet(key, value)); + db.Execute("SETWITHETAG", [key, value]); try { db.StringSetRange(key, -1, newValue); @@ -1863,27 +1373,7 @@ public void SetRangeTest() } [Test] - public void PingTest() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string result = (string)db.Execute("PING"); - ClassicAssert.AreEqual("PONG", result); - } - - [Test] - public void AskingTest() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string result = (string)db.Execute("ASKING"); - ClassicAssert.AreEqual("OK", result); - } - - [Test] - public void KeepTtlTest() + public void KeepTtlTestForDataInitiallySetWithEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1891,8 +1381,8 @@ public void KeepTtlTest() int expire = 3; var keyA = "keyA"; var keyB = "keyB"; - db.StringSet(keyA, keyA); - db.StringSet(keyB, keyB); + db.Execute("SETWITHETAG", [keyA, keyA]); + db.Execute("SETWITHETAG", [keyB, keyB]); db.KeyExpire(keyA, TimeSpan.FromSeconds(expire)); db.KeyExpire(keyB, TimeSpan.FromSeconds(expire)); @@ -1907,25 +1397,30 @@ public void KeepTtlTest() Thread.Sleep(expire * 1000 + 100); - string value = db.StringGet(keyA); - ClassicAssert.AreEqual(null, value); - - value = db.StringGet(keyB); + string value = db.StringGet(keyB); ClassicAssert.AreEqual(keyB, value); + + value = db.StringGet(keyA); + ClassicAssert.AreEqual(null, value); } [Test] - public void StrlenTest() + public void StrlenTestOnEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - ClassicAssert.IsTrue(db.StringSet("mykey", "foo bar")); - ClassicAssert.IsTrue(db.StringLength("mykey") == 7); - ClassicAssert.IsTrue(db.StringLength("nokey") == 0); + + db.Execute("SETWITHETAG", ["mykey", "foo bar"]); + + ClassicAssert.AreEqual(7, db.StringLength("mykey")); + ClassicAssert.AreEqual(0, db.StringLength("nokey")); + + var etagToCheck = db.Execute("GETWITHETAG", "mykey"); + ClassicAssert.AreEqual(0, long.Parse(etagToCheck[0].ToString())); } [Test] - public void TTLTestMilliseconds() + public void TTLTestMillisecondsForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1937,7 +1432,8 @@ public void TTLTestMilliseconds() var pttl = db.Execute("PTTL", key); ClassicAssert.AreEqual(-2, (int)pttl); - db.StringSet(key, val); + db.Execute("SETWITHETAG", [key, val]); + pttl = db.Execute("PTTL", key); ClassicAssert.AreEqual(-1, (int)pttl); @@ -1957,10 +1453,14 @@ public void TTLTestMilliseconds() var ttl = db.KeyTimeToLive(key); ClassicAssert.IsNull(ttl); + + // nothing should have affected the etag in the above commands + long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(0, etagToCheck); } [Test] - public void GetDelTest() + public void GetDelTestForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1969,7 +1469,8 @@ public void GetDelTest() var val = "myKeyValue"; // Key Setup - db.StringSet(key, val); + db.Execute("SETWITHETAG", [key, val]); + var retval = db.StringGet(key); ClassicAssert.AreEqual(val, retval.ToString()); @@ -1987,7 +1488,10 @@ public void GetDelTest() // Key setup with metadata key = "myKeyWithMetadata"; val = "myValueWithMetadata"; - db.StringSet(key, val, expiry: TimeSpan.FromSeconds(10000)); + + db.Execute("SETWITHETAG", [key, val]); + db.KeyExpire(key, TimeSpan.FromSeconds(10000)); + retval = db.StringGet(key); ClassicAssert.AreEqual(val, retval.ToString()); @@ -2000,7 +1504,7 @@ public void GetDelTest() } [Test] - public void AppendTest() + public void AppendTestForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -2009,21 +1513,32 @@ public void AppendTest() var val = "myKeyValue"; var val2 = "myKeyValue2"; - db.StringSet(key, val); + db.Execute("SETWITHETAG", [key, val]); + var len = db.StringAppend(key, val2); ClassicAssert.AreEqual(val.Length + val2.Length, len); var _val = db.StringGet(key); ClassicAssert.AreEqual(val + val2, _val.ToString()); + long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(1, etagToCheck); + + db.KeyDelete(key); + // Test appending an empty string - db.StringSet(key, val); + db.Execute("SETWITHETAG", [key, val]); + var len1 = db.StringAppend(key, ""); ClassicAssert.AreEqual(val.Length, len1); _val = db.StringGet(key); ClassicAssert.AreEqual(val, _val.ToString()); + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + // we appended nothing so this remains 0 + ClassicAssert.AreEqual(0, etagToCheck); + // Test appending to a non-existent key var nonExistentKey = "nonExistentKey"; var len2 = db.StringAppend(nonExistentKey, val2); @@ -2032,15 +1547,24 @@ public void AppendTest() _val = db.StringGet(nonExistentKey); ClassicAssert.AreEqual(val2, _val.ToString()); + db.KeyDelete(key); + // Test appending to a key with a large value var largeVal = new string('a', 1000000); - db.StringSet(key, largeVal); + db.Execute("SETWITHETAG", [ key, largeVal ]); var len3 = db.StringAppend(key, val2); ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); + + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(1, etagToCheck); // Test appending to a key with metadata var keyWithMetadata = "keyWithMetadata"; - db.StringSet(keyWithMetadata, val, TimeSpan.FromSeconds(10000)); + db.Execute("SETWITHETAG", [keyWithMetadata, val]); + db.KeyExpire(keyWithMetadata, TimeSpan.FromSeconds(10000)); + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [keyWithMetadata]))[0].ToString()); + ClassicAssert.AreEqual(0, etagToCheck); + var len4 = db.StringAppend(keyWithMetadata, val2); ClassicAssert.AreEqual(val.Length + val2.Length, len4); @@ -2049,171 +1573,170 @@ public void AppendTest() var time = db.KeyTimeToLive(keyWithMetadata); ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); - } + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [keyWithMetadata]))[0].ToString()); + ClassicAssert.AreEqual(1, etagToCheck); + } [Test] - public void AppendTestEtagsetData() + public void SetBitOperationsOnEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - var key = "myKey"; - var val = "myKeyValue"; - var val2 = "myKeyValue2"; + string key = "miki"; + // 64 BIT BITMAP + Byte[] initialBitmap = new byte[8]; + string bitMapAsStr = Encoding.UTF8.GetString(initialBitmap);; - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SETWITHETAG", [ key, bitMapAsStr]); - var len = db.StringAppend(key, val2); - ClassicAssert.AreEqual(val.Length + val2.Length, len); + long setbits = db.StringBitCount(key); + ClassicAssert.AreEqual(0, setbits); - var _val = db.StringGet(key); - ClassicAssert.AreEqual(val + val2, _val.ToString()); + long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(0, etagToCheck); - // Test appending an empty string - db.KeyDelete(key); - db.Execute("SETWITHETAG", [key, val]); - var len1 = db.StringAppend(key, ""); - ClassicAssert.AreEqual(val.Length, len1); + // set all 64 bits one by one + var expectedBitCount = 0; + for (int i = 0; i < 64; i++) + { + // SET the ith bit in the bitmap + bool originalValAtBit = db.StringSetBit(key, i, true); + ClassicAssert.IsFalse(originalValAtBit); - _val = db.StringGet(key); - ClassicAssert.AreEqual(val, _val.ToString()); + expectedBitCount++; - // Test appending to a non-existent key - var nonExistentKey = "nonExistentKey"; - var len2 = db.StringAppend(nonExistentKey, val2); - ClassicAssert.AreEqual(val2.Length, len2); + bool currentBitVal = db.StringGetBit(key, i); + ClassicAssert.IsTrue(currentBitVal); - _val = db.StringGet(nonExistentKey); - ClassicAssert.AreEqual(val2, _val.ToString()); + setbits = db.StringBitCount(key); + ClassicAssert.AreEqual(expectedBitCount, setbits); - // Test appending to a key with a large value - var largeVal = new string('a', 1000000); - db.KeyDelete(key); - db.Execute("SETWITHETAG", [ key, largeVal ]); - var len3 = db.StringAppend(key, val2); - ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); + // with each bit set that we do, we are increasing the etag as well by 1 + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(expectedBitCount, etagToCheck); + } - // Test appending to a key with metadata - var keyWithMetadata = "keyWithMetadata"; - db.Execute("SETWITHETAG", [ key, val]); - db.KeyExpire(key, TimeSpan.FromSeconds(10000)); + var expectedEtag = expectedBitCount; + // unset all 64 bits one by one in reverse order + for (int i = 63; i > -1; i--) + { + bool originalValAtBit = db.StringSetBit(key, i, false); + ClassicAssert.IsTrue(originalValAtBit); - var len4 = db.StringAppend(keyWithMetadata, val2); - ClassicAssert.AreEqual(val.Length + val2.Length, len4); + expectedEtag++; + expectedBitCount--; - _val = db.StringGet(keyWithMetadata); - ClassicAssert.AreEqual(val + val2, _val.ToString()); + bool currentBitVal = db.StringGetBit(key, i); + ClassicAssert.IsFalse(currentBitVal); - var time = db.KeyTimeToLive(keyWithMetadata); - ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); + setbits = db.StringBitCount(key); + ClassicAssert.AreEqual(expectedBitCount, setbits); + + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(expectedEtag, etagToCheck); + } } [Test] - public void HelloTest1() + public void BitFieldSetGetOnEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - // Test "HELLO 2" - var result = db.Execute("HELLO", "2"); - - ClassicAssert.IsNotNull(result); - ClassicAssert.AreEqual(ResultType.Array, result.Resp2Type); - ClassicAssert.AreEqual(ResultType.Array, result.Resp3Type); - var resultDict = result.ToDictionary(); - ClassicAssert.IsNotNull(resultDict); - ClassicAssert.AreEqual(2, (int)resultDict["proto"]); - ClassicAssert.AreEqual("master", (string)resultDict["role"]); - - // Test "HELLO 3" - result = db.Execute("HELLO", "3"); - - ClassicAssert.IsNotNull(result); - ClassicAssert.AreEqual(ResultType.Array, result.Resp2Type); - ClassicAssert.AreEqual(ResultType.Map, result.Resp3Type); - resultDict = result.ToDictionary(); - ClassicAssert.IsNotNull(resultDict); - ClassicAssert.AreEqual(3, (int)resultDict["proto"]); - ClassicAssert.AreEqual("master", (string)resultDict["role"]); + var key = "mewo"; + + // Arrange - Set an 8-bit unsigned value at offset 0 + db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte + + // Act - Set value to 127 (binary: 01111111) + db.Execute("BITFIELD", key, "SET", "u8", "0", "127"); + + long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(1, etagToCheck); + + // Get value back + var getResult = (RedisResult[])db.Execute("BITFIELD", key, "GET", "u8", "0"); + + // Assert + ClassicAssert.AreEqual(127, (long)getResult[0]); // Ensure the value set was retrieved correctly } [Test] - public void AsyncTest1() + public void BitFieldIncrementWithWrapOverflowOnEtagSetData() { - // Set up low-memory database - TearDown(); - TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, lowMemory: true, DisableObjects: true); - server.Start(); + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); - string firstKey = null, firstValue = null, lastKey = null, lastValue = null; + var key = "mewo"; - // Load the data so that it spills to disk - using (var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig())) - { - var db = redis.GetDatabase(0); + // Arrange - Set an 8-bit unsigned value at offset 0 + db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte + + // Act - Set initial value to 255 and try to increment by 1 + db.Execute("BITFIELD", key, "SET", "u8", "0", "255"); + long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(1, etagToCheck); - int keyCount = 5; - int valLen = 256; - int keyLen = 8; + var incrResult = db.Execute("BITFIELD", key, "INCRBY", "u8", "0", "1"); - List> data = []; - for (int i = 0; i < keyCount; i++) - { - lastKey = GetRandomString(keyLen); - lastValue = GetRandomString(valLen); - if (firstKey == null) - { - firstKey = lastKey; - firstValue = lastValue; - } - data.Add(new Tuple(lastKey, lastValue)); - var pair = data.Last(); - db.StringSet(pair.Item1, pair.Item2); - } - } + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(2, etagToCheck); + + // Assert + ClassicAssert.AreEqual(0, (long)incrResult); // Should wrap around and return 0 + } + + [Test] + public void BitFieldIncrementWithSaturateOverflowOnEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "mewo"; + + // Arrange - Set an 8-bit unsigned value at offset 0 + db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte + + // Act - Set initial value to 250 and try to increment by 10 with saturate overflow + db.Execute("BITFIELD", key, "SET", "u8", "0", "250"); + + long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(1, etagToCheck); + + var incrResult = db.Execute("BITFIELD", key, "OVERFLOW", "SAT", "INCRBY", "u8", "0", "10"); + + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + ClassicAssert.AreEqual(2, etagToCheck); - // We use newline counting for HELLO response as the exact length can vary slightly across versions - using var lightClientRequest = TestUtils.CreateRequest(countResponseType: CountResponseType.Newlines); - - var expectedNewlineCount = 32; // 32 '\n' characters expected in response - var response = lightClientRequest.Execute($"hello 3", expectedNewlineCount); - ClassicAssert.IsTrue(response.Length is > 180 and < 190); - - // Switch to byte counting in response - lightClientRequest.countResponseType = CountResponseType.Bytes; - - // Turn on async - var expectedResponse = "+OK\r\n"; - response = lightClientRequest.Execute($"async on", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); - - // Get in-memory data item - expectedResponse = $"${lastValue.Length}\r\n{lastValue}\r\n"; - response = lightClientRequest.Execute($"GET {lastKey}", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); - - // Get disk data item with async on - expectedResponse = $"-ASYNC 0\r\n>3\r\n$5\r\nasync\r\n$1\r\n0\r\n${firstValue.Length}\r\n{firstValue}\r\n"; - response = lightClientRequest.Execute($"GET {firstKey}", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); - - // Issue barrier command for async - expectedResponse = "+OK\r\n"; - response = lightClientRequest.Execute($"async barrier", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); - - // Turn off async - expectedResponse = "+OK\r\n"; - response = lightClientRequest.Execute($"async off", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); - - // Get disk data item with async off - expectedResponse = $"${firstValue.Length}\r\n{firstValue}\r\n"; - response = lightClientRequest.Execute($"GET {firstKey}", expectedResponse.Length); - ClassicAssert.AreEqual(expectedResponse, response); + // Assert + ClassicAssert.AreEqual(255, (long)incrResult); // Should saturate at the max value of 255 for u8 } + + [Test] + public void HyperLogLogCommandsShouldReturnWrongTypeErrorForEtagSetData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "mewo"; + var key2 = "dude"; + + db.Execute("SETWITHETAG", [key, "mars"]); + db.Execute("SETWITHETAG", [key2, "marsrover"]); + + RedisServerException ex = Assert.Throws(() => db.Execute("PFADD", [key, "woohoo"])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE_HLL), ex.Message); + + ex = Assert.Throws(() => db.Execute("PFMERGE", [key, key2])); + + ClassicAssert.IsNotNull(ex); + ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE_HLL), ex.Message); + } + #endregion } } \ No newline at end of file diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 219a3d9827..ac5a60fff8 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -431,6 +431,7 @@ public void SetGet() ClassicAssert.IsNull(expiry); } + [Test] public void SetExpiryIncr() { diff --git a/test/Garnet.test/TransactionTests.cs b/test/Garnet.test/TransactionTests.cs index 7df47511d4..76e91799b1 100644 --- a/test/Garnet.test/TransactionTests.cs +++ b/test/Garnet.test/TransactionTests.cs @@ -304,8 +304,6 @@ public async Task WatchKeyFromDisk() ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); } - // HK TODO: Add test here for running etag cmds in a transaction - private static void updateKey(string key, string value) { using var lightClientRequest = TestUtils.CreateRequest(); From 683ead2d6058d0d744d2b64b94b15635c0b5f773 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Sep 2024 09:28:11 +1000 Subject: [PATCH 04/87] add edge case handling for etag override set --- .../Storage/Functions/MainStore/RMWMethods.cs | 31 ++++++++++++ .../Functions/MainStore/VarLenInputMethods.cs | 4 +- test/Garnet.test/RespEtagTests.cs | 47 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 8c200de33f..2d7f5a7a20 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -493,6 +493,24 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span rmwInfo.Action = RMWAction.ExpireAndStop; return false; + case RespCommand.SETWITHETAG: + if (input.Length - RespInputHeader.Size + sizeof(long) > value.Length) + return false; + + recordInfo.SetHasETag(); + + // Copy input to value + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + sizeof(long)); + value.ExtraMetadata = input.ExtraMetadata; + + // initial etag set to 0, this is a counter based etag that is incremented on change + *(long*)value.ToPointer() = 0; + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); + + // Copy initial etag to output + CopyRespNumber(0, ref output); + // early return since initial etag setting does not need to be incremented + return true;; case RespCommand.APPEND: // If nothing to append, can avoid copy update. @@ -652,6 +670,19 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa switch (cmd) { + case RespCommand.SETWITHETAG: + Debug.Assert(input.Length - RespInputHeader.Size + sizeof(long) == newValue.Length); + // initial etag setting so does not need to be incremented + shouldUpdateEtag = false; + recordInfo.SetHasETag(); + // Copy input to value + newValue.ExtraMetadata = input.ExtraMetadata; + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(sizeof(long))); + // initial Etag + *(long*)newValue.ToPointer() = 0; + // Copy initial etag to output + CopyRespNumber(0, ref output); + break; case RespCommand.SETIFMATCH: Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data."); diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 03e906944e..a177bd5019 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -173,7 +173,9 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool ha case RespCommand.SETIFMATCH: case RespCommand.PERSIST: break; - + case RespCommand.SETWITHETAG: + // same space as SET but with 8 additional bytes for etag at the front of the payload + return sizeof(int) + input.Length - RespInputHeader.Size + sizeof(long); case RespCommand.EXPIRE: case RespCommand.PEXPIRE: return sizeof(int) + t.Length + input.MetadataSize; diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 3c1fefc488..44a396f78b 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -137,6 +137,53 @@ public void GetIfNotMatchReturnsDataWhenEtagDoesNotMatch() #endregion + # region Edgecases + + [Test] + public void SetWithEtagOnAlreadyExistingDataOverridesIt() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + ClassicAssert.IsTrue(db.StringSet("rizz", "used")); + + // inplace update + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + long etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); + + db.KeyDelete("rizz"); + + ClassicAssert.IsTrue(db.StringSet("rizz", "my")); + + // Copy update + res = db.Execute("SETWITHETAG", ["rizz", "some"]); + etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); + } + + [Test] + public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesIt() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + long etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); + + // inplace update + res = db.Execute("SETWITHETAG", ["rizz", "meow"]); + etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); + + // Copy update + res = db.Execute("SETWITHETAG", ["rizz", "oneofus"]); + etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); + } + + #endregion + #region ETAG Apis with non-etag data // ETAG Apis with non-Etag data just tests that in all scenarios we always return wrong data type response From 7078ec3f7f739c8f1da2b118852c4776c1c63a8e Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Sep 2024 11:57:37 +1000 Subject: [PATCH 05/87] Add documentation --- website/docs/commands/etag-commands.md | 145 +++++++ website/sidebars.js | 2 +- website/yarn.lock | 580 ++++++++++++------------- 3 files changed, 423 insertions(+), 304 deletions(-) create mode 100644 website/docs/commands/etag-commands.md diff --git a/website/docs/commands/etag-commands.md b/website/docs/commands/etag-commands.md new file mode 100644 index 0000000000..87e6a0fb75 --- /dev/null +++ b/website/docs/commands/etag-commands.md @@ -0,0 +1,145 @@ +--- +id: etag-commands +sidebar_label: ETags +title: ETAG +slug: etag +--- + +--- + +## ETag Support + +Garnet provides support for ETags on raw strings. By using the ETag-related commands outlined below, you can associate any string-based key-value pair inserted into Garnet with an automatically updated ETag. + +Compatibility with non-ETag commands and the behavior of data inserted with ETags are detailed at the end of this document. + +--- + +### **SETWITHETAG** + +#### **Syntax** + +```bash +SETWITHETAG key value +``` + +Inserts a key-value string pair into Garnet, associating an ETag that will be updated upon changes to the value. + +#### **Response** + +One of the following: + +- **Integer reply**: A response integer indicating the initial ETag value on success. +- **Error reply**: Returns an error if the key already exists. + +--- + +### **GETWITHETAG** + +#### **Syntax** + +```bash +GETWITHETAG key +``` + +Retrieves the value and the ETag associated with the given key. + +#### **Response** + +One of the following: + +- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. +- **Nil reply**: If the key does not exist. +- **Error reply**: Returns an error if `GETWITHETAG` is called on a key that was not set with `SETWITHETAG`. + +--- + +### **SETIFMATCH** + +#### **Syntax** + +```bash +SETIFMATCH key value etag +``` + +Updates the value of a key if the provided ETag matches the current ETag of the key. + +#### **Response** + +One of the following: + +- **Integer reply**: The updated ETag if the value was successfully updated. +- **Nil reply**: If the key does not exist. +- **Error reply (ETag mismatch)**: If the provided ETag does not match the current ETag. +- **Error reply**: Returns an error if `SETIFMATCH` is called on a key not set with `SETWITHETAG`. + +--- + +### **GETIFNOTMATCH** + +#### **Syntax** + +```bash +GETIFNOTMATCH key etag +``` + +Retrieves the value if the ETag associated with the key has changed; otherwise, returns a response indicating no change. + +#### **Response** + +One of the following: + +- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. +- **Nil reply**: If the key does not exist. +- **Simple string reply**: Returns a string indicating the value is unchanged if the provided ETag matches the current ETag. +- **Error reply**: Returns an error if `GETIFNOTMATCH` is called on a key not set with `SETWITHETAG`. + +--- + +## Compatibility and Behavior with Non-ETag Commands + +ETag commands executed on keys that were not set with `SETWITHETAG` will return a type mismatch error. Additionally, invoking `SETWITHETAG` on an existing key will overwrite the key-value pair and reset the associated ETag. + +Below is the expected behavior of ETag-associated key-value pairs when non-ETag commands are used. + +- **SET, MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. +- **RENAME**: Renaming an ETag-associated key-value pair will reset the ETag to 0 for the renamed key. + +--- + +### **Same Behavior as Non-ETag Key-Value Pairs** + +The following commands do not expose the ETag to the user and behave the same as non-ETag key-value pairs. From the user's perspective, there is no indication that a key-value pair is associated with an ETag. + +- **GET** +- **DEL** +- **EXISTS** +- **EXPIRE** +- **PEXPIRE** +- **PERSIST** +- **GETRANGE** +- **TTL** +- **PTTL** +- **GETDEL** +- **STRLEN** +- **GETBIT** +- **BITCOUNT** +- **BITPOS** +- **BITFIELD_RO** + +### **Commands That Update ETag Internally** + +The following commands update the underlying data and consequently update the ETag of the key-value pair. However, the new ETag will not be exposed to the user until explicitly retrieved via an ETag-related command. + +- **SETRANGE** +- **APPEND** +- **INCR** +- **INCRBY** +- **DECR** +- **DECRBY** +- **SETBIT** +- **UNLINK** +- **MGET** +- **BITFIELD** + +--- \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index ccca441189..2e0bcfaa50 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -20,7 +20,7 @@ const sidebars = { {type: 'category', label: 'Welcome', collapsed: false, items: ["welcome/intro", "welcome/news", "welcome/features", "welcome/releases", "welcome/compatibility", "welcome/roadmap", "welcome/faq", "welcome/about-us"]}, {type: 'category', label: 'Getting Started', items: ["getting-started/build", "getting-started/configuration", "getting-started/memory", "getting-started/security", "getting-started/compaction"]}, {type: 'category', label: 'Benchmarking', items: ["benchmarking/overview", "benchmarking/results-resp-bench", "benchmarking/resp-bench"]}, - {type: 'category', label: 'Commands', items: ["commands/overview", "commands/api-compatibility", "commands/raw-string", "commands/generic-commands", "commands/analytics-commands", "commands/data-structures", "commands/server-commands", "commands/client-commands", "commands/checkpoint-commands", "commands/transactions-commands", "commands/cluster", "commands/acl-commands", "commands/scripting-commands"]}, + {type: 'category', label: 'Commands', items: ["commands/overview", "commands/api-compatibility", "commands/raw-string", "commands/etag-commands", "commands/generic-commands", "commands/analytics-commands", "commands/data-structures", "commands/server-commands", "commands/client-commands", "commands/checkpoint-commands", "commands/transactions-commands", "commands/cluster", "commands/acl-commands", "commands/scripting-commands"]}, {type: 'category', label: 'Server Extensions', items: ["extensions/overview", "extensions/raw-strings", "extensions/objects", "extensions/transactions", "extensions/procedure", "extensions/module"]}, {type: 'category', label: 'Cluster Mode', items: ["cluster/overview", "cluster/replication", "cluster/key-migration"]}, {type: 'category', label: 'Developer Guide', items: ["dev/onboarding", "dev/code-structure", "dev/configuration", "dev/network", "dev/processing", "dev/garnet-api", diff --git a/website/yarn.lock b/website/yarn.lock index b9589fbad7..782caaf496 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -75,11 +75,6 @@ "@algolia/requester-common" "4.24.0" "@algolia/transporter" "4.24.0" -"@algolia/client-common@5.4.1": - version "5.4.1" - resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.4.1.tgz" - integrity sha512-IffPD+CETiR8YJMVC1lcjnhETLpJ2L0ORZCbbRvwo/S11D1j/keR7AqKVMn4TseRJCfjmBFOcFrC+m4sXjyQWA== - "@algolia/client-personalization@4.24.0": version "4.24.0" resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.24.0.tgz" @@ -89,16 +84,6 @@ "@algolia/requester-common" "4.24.0" "@algolia/transporter" "4.24.0" -"@algolia/client-search@>= 4.9.1 < 6": - version "5.4.1" - resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.4.1.tgz" - integrity sha512-nCgWY2p0tZgBqJKmA5E6B3VW+7uqxi1Orf88zNWOihJBRFeOV932pzG4vGrX9l0+p0o/vJabYxuomO35rEt5dw== - dependencies: - "@algolia/client-common" "5.4.1" - "@algolia/requester-browser-xhr" "5.4.1" - "@algolia/requester-fetch" "5.4.1" - "@algolia/requester-node-http" "5.4.1" - "@algolia/client-search@4.24.0": version "4.24.0" resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz" @@ -149,25 +134,11 @@ dependencies: "@algolia/requester-common" "4.24.0" -"@algolia/requester-browser-xhr@5.4.1": - version "5.4.1" - resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.4.1.tgz" - integrity sha512-J6+YfU+maR0nIbsYRHoq0UpneilX97hrZzPuuvSoBojQmPo8PeCXKGeT/F0D8uFI6G4CMTKEPGmQYrC9IpCbcQ== - dependencies: - "@algolia/client-common" "5.4.1" - "@algolia/requester-common@4.24.0": version "4.24.0" resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz" integrity sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA== -"@algolia/requester-fetch@5.4.1": - version "5.4.1" - resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.4.1.tgz" - integrity sha512-AO/C1pqqpIS8p2IsfM5x92S+UBKkcIen5dHfMEh1rnV0ArWDreeqrtxMD2A+6AjQVwYeZNy56w7o7PVIm6mc8g== - dependencies: - "@algolia/client-common" "5.4.1" - "@algolia/requester-node-http@4.24.0": version "4.24.0" resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz" @@ -175,13 +146,6 @@ dependencies: "@algolia/requester-common" "4.24.0" -"@algolia/requester-node-http@5.4.1": - version "5.4.1" - resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.4.1.tgz" - integrity sha512-2Y3vffc91egwFxz0SjXFEH4q8nvlNJHcz+0//NaWItRU68AvD+3aI/j66STPjkLQOC0Ku6ckA9ChhbOVfrv+Uw== - dependencies: - "@algolia/client-common" "5.4.1" - "@algolia/transporter@4.24.0": version "4.24.0" resolved "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz" @@ -212,7 +176,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.21.3", "@babel/core@^7.23.3", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0": +"@babel/core@^7.21.3", "@babel/core@^7.23.3": version "7.24.0" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz" integrity sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw== @@ -1279,7 +1243,7 @@ "@docsearch/css" "3.6.1" algoliasearch "^4.19.1" -"@docusaurus/core@^3.4.0", "@docusaurus/core@3.5.2": +"@docusaurus/core@3.5.2", "@docusaurus/core@^3.4.0": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz" integrity sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w== @@ -1438,7 +1402,7 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@*", "@docusaurus/plugin-content-docs@^2 || ^3", "@docusaurus/plugin-content-docs@3.5.2": +"@docusaurus/plugin-content-docs@3.5.2", "@docusaurus/plugin-content-docs@^2 || ^3": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz" integrity sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ== @@ -1583,7 +1547,7 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@^2 || ^3", "@docusaurus/theme-common@3.5.2": +"@docusaurus/theme-common@3.5.2": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.5.2.tgz" integrity sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew== @@ -1636,7 +1600,7 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@^2 || ^3", "@docusaurus/theme-translations@3.5.2": +"@docusaurus/theme-translations@3.5.2", "@docusaurus/theme-translations@^2 || ^3": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz" integrity sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw== @@ -1644,7 +1608,7 @@ fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/types@*", "@docusaurus/types@^3.0.0", "@docusaurus/types@3.5.2": +"@docusaurus/types@3.5.2", "@docusaurus/types@^3.0.0": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz" integrity sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw== @@ -1659,14 +1623,14 @@ webpack "^5.88.1" webpack-merge "^5.9.0" -"@docusaurus/utils-common@^2 || ^3", "@docusaurus/utils-common@3.5.2": +"@docusaurus/utils-common@3.5.2", "@docusaurus/utils-common@^2 || ^3": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz" integrity sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg== dependencies: tslib "^2.6.0" -"@docusaurus/utils-validation@^2 || ^3", "@docusaurus/utils-validation@3.5.2": +"@docusaurus/utils-validation@3.5.2", "@docusaurus/utils-validation@^2 || ^3": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz" integrity sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA== @@ -1680,7 +1644,7 @@ lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/utils@^2 || ^3", "@docusaurus/utils@3.5.2": +"@docusaurus/utils@3.5.2", "@docusaurus/utils@^2 || ^3": version "3.5.2" resolved "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz" integrity sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA== @@ -1736,6 +1700,28 @@ mark.js "^8.11.1" tslib "^2.4.0" +"@emnapi/core@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.2.0.tgz#7b738e5033738132bf6af0b8fae7b05249bdcbd7" + integrity sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w== + dependencies: + "@emnapi/wasi-threads" "1.0.1" + tslib "^2.4.0" + +"@emnapi/runtime@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.2.0.tgz#71d018546c3a91f3b51106530edbc056b9f2f2e3" + integrity sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz#d7ae71fd2166b1c916c6cd2d0df2ef565a2e1a5b" + integrity sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw== + dependencies: + tslib "^2.4.0" + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz" @@ -1848,6 +1834,55 @@ dependencies: "@types/mdx" "^2.0.0" +"@napi-rs/wasm-runtime@^0.2.3": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz#d27788176f250d86e498081e3c5ff48a17606918" + integrity sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ== + dependencies: + "@emnapi/core" "^1.1.0" + "@emnapi/runtime" "^1.1.0" + "@tybys/wasm-util" "^0.9.0" + +"@node-rs/jieba-android-arm-eabi@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.10.3.tgz#821af26a4953b3fbdf2f80a4d08a9d9114b40bea" + integrity sha512-fuqVtaYlUKZg3cqagYFxj1DSa7ZHKXLle4iGH2kbQWg7Kw6cf7aCYBHIUZuH5sliK10M/CWccZ+SGRUwcSGfbg== + +"@node-rs/jieba-android-arm64@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-android-arm64/-/jieba-android-arm64-1.10.3.tgz#e5c285fb8de71739dfa3a83d894adcadb799c404" + integrity sha512-iuZZZq5yD9lT+AgaXpFe19gtAsIecUODRLLaBFbavjgjLk5cumv38ytWjS36s/eqptwI15MQfysSYOlWtMEG5g== + +"@node-rs/jieba-darwin-arm64@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-darwin-arm64/-/jieba-darwin-arm64-1.10.3.tgz#67df85df39ff60dcc3e084f6e36e5182779b69ad" + integrity sha512-dwPhkav1tEARskwPz91UUXL2NXy4h0lJYTuJzpGgwXxm552zBM2JJ41kjah1364j+EOq5At3NQvf5r5rH89phQ== + +"@node-rs/jieba-darwin-x64@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-darwin-x64/-/jieba-darwin-x64-1.10.3.tgz#ffdc8a63335294d7c68d3aebec870ec0824ebe98" + integrity sha512-kjxvV6G1baQo/2I3mELv5qGv4Q0rhd5srwXhypSxMWZFtSpNwCDsLcIOR5bvMBci6QVFfZOs6WD6DKiWVz0SlA== + +"@node-rs/jieba-freebsd-x64@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-freebsd-x64/-/jieba-freebsd-x64-1.10.3.tgz#188349a9074b200af4a3e8a0ea169f45efd6c162" + integrity sha512-QYTsn+zlWRil+MuBeLfTK5Md4GluOf2lHnFqjrOZW2oMgNOvxB3qoLV4TUf70S/E2XHeP6PUdjCKItX8C7GQPg== + +"@node-rs/jieba-linux-arm-gnueabihf@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-arm-gnueabihf/-/jieba-linux-arm-gnueabihf-1.10.3.tgz#e1831b7b08a32904b12860555978c50222a97b54" + integrity sha512-UFB43kDOvqmbRl99e3GPwaTuwJZaAvgLaMTvBkmxww4MpQH6G1k31RLzMW/S21uSQso2lj6W/Mm59gaJk2FiyA== + +"@node-rs/jieba-linux-arm64-gnu@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-arm64-gnu/-/jieba-linux-arm64-gnu-1.10.3.tgz#326712eb7418f9796b113af93afe59ab64c37add" + integrity sha512-bu++yWi10wZtnS5uLcwxzxKmHVT77NgQMK8JiQr1TWCl3Y1Th7CnEHQtxfVB489edDK8l644h1/4zSTe5fRnOQ== + +"@node-rs/jieba-linux-arm64-musl@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-linux-arm64-musl/-/jieba-linux-arm64-musl-1.10.3.tgz#6a3149d5abbe09f7c7748da219d5c39522b36c8a" + integrity sha512-pJh+SzrK1HaKakhdFM+ew9vXwpZqMxy9u0U7J4GT+3GvOwnAZ+KjeaHebIfgOz7ZHvp/T4YBNf8oWW4zwj3AJw== + "@node-rs/jieba-linux-x64-gnu@1.10.3": version "1.10.3" resolved "https://registry.npmjs.org/@node-rs/jieba-linux-x64-gnu/-/jieba-linux-x64-gnu-1.10.3.tgz" @@ -1858,6 +1893,28 @@ resolved "https://registry.npmjs.org/@node-rs/jieba-linux-x64-musl/-/jieba-linux-x64-musl-1.10.3.tgz" integrity sha512-h45HMVU/hgzQ0saXNsK9fKlGdah1i1cXZULpB5vQRlRL2ZIaGp+ULtWTogS7vkoo2K8s2l4tqakWMg9eUjIJ2A== +"@node-rs/jieba-wasm32-wasi@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-wasm32-wasi/-/jieba-wasm32-wasi-1.10.3.tgz#b852eb2c9b8c81c5514ed8bb76d74c1cdf66fe76" + integrity sha512-vuoQ62vVoedNGcBmIi4UWdtNBOZG8B+vDYfjx3FD6rNg6g/RgwbVjYXbOVMOQwX06Ob9CfrutICXdUGHgoxzEQ== + dependencies: + "@napi-rs/wasm-runtime" "^0.2.3" + +"@node-rs/jieba-win32-arm64-msvc@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-win32-arm64-msvc/-/jieba-win32-arm64-msvc-1.10.3.tgz#eefce48df8ec0496a0e45593d0b5f8981bb32b80" + integrity sha512-B8t4dh56TZnMLBoYWDkopf1ed37Ru/iU1qiIeBkbZWXGmNBChNZUOd//eaPOFjx8m9Sfc8bkj3FBRWt/kTAhmw== + +"@node-rs/jieba-win32-ia32-msvc@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-win32-ia32-msvc/-/jieba-win32-ia32-msvc-1.10.3.tgz#edfb74e880a32f66a6810502957b62f9b042b487" + integrity sha512-SKuPGZJ5T+X4jOn1S8LklOSZ6HC7UBiw0hwi2z9uqX6WgElquLjGi/xfZ2gPqffeR/5K/PUu7aqYUUPL1XonVQ== + +"@node-rs/jieba-win32-x64-msvc@1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@node-rs/jieba-win32-x64-msvc/-/jieba-win32-x64-msvc-1.10.3.tgz#285a24134d9c367b11d73060bdc37c351c3e60b5" + integrity sha512-j9I4+a/tf2hsLu8Sr0NhcLBVNBBQctO2mzcjemMpRa1SlEeODyic9RIyP8Ljz3YTN6MYqKh1KA9iR1xvxjxYFg== + "@node-rs/jieba@^1.6.0": version "1.10.3" resolved "https://registry.npmjs.org/@node-rs/jieba/-/jieba-1.10.3.tgz" @@ -1886,7 +1943,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -2020,7 +2077,7 @@ "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" "@svgr/babel-plugin-transform-svg-component" "8.0.0" -"@svgr/core@*", "@svgr/core@8.1.0": +"@svgr/core@8.1.0": version "8.1.0" resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== @@ -2084,6 +2141,13 @@ resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@tybys/wasm-util@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" + integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== + dependencies: + tslib "^2.4.0" + "@types/acorn@^4.0.0": version "4.0.6" resolved "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz" @@ -2339,7 +2403,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@>= 16.8.0 < 19.0.0", "@types/react@>=16": +"@types/react@*": version "18.3.6" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.6.tgz" integrity sha512-CnGaRYNu2iZlkGXGrOYtdg5mLK8neySj0woZ4e2wF/eli2E6Sazmq5X+Nrj6OBrrFVQfJWTUFeqAzoRhWQXYvg== @@ -2424,7 +2488,7 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@^1.12.1", "@webassemblyjs/ast@1.12.1": +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz" integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== @@ -2525,7 +2589,7 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@^1.12.1", "@webassemblyjs/wasm-parser@1.12.1": +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz" integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== @@ -2578,7 +2642,7 @@ acorn-walk@^8.0.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: +acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: version "8.11.3" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -2603,12 +2667,7 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1: - version "3.5.2" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^3.5.2: +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2620,7 +2679,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.2, ajv@^6.12.5, ajv@^6.9.1: +ajv@^6.12.2, ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2630,7 +2689,7 @@ ajv@^6.12.2, ajv@^6.12.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.9.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -2647,7 +2706,7 @@ algoliasearch-helper@^3.13.3: dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1, "algoliasearch@>= 3.1 < 6", "algoliasearch@>= 4.9.1 < 6": +algoliasearch@^4.18.0, algoliasearch@^4.19.1: version "4.24.0" resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.24.0.tgz" integrity sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g== @@ -2904,7 +2963,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.22.3, browserslist@^4.23.0, "browserslist@>= 4.21.0": +browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.22.3, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -3060,6 +3119,19 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" +cheerio@1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + cheerio@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz" @@ -3077,19 +3149,6 @@ cheerio@^1.0.0: undici "^6.19.5" whatwg-mimetype "^4.0.0" -cheerio@1.0.0-rc.12: - version "1.0.0-rc.12" - resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz" - integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.0.1" - htmlparser2 "^8.0.1" - parse5 "^7.0.0" - parse5-htmlparser2-tree-adapter "^7.0.0" - chokidar@^3.4.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" @@ -3179,16 +3238,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colord@^2.9.3: version "2.9.3" resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" @@ -3209,6 +3268,11 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== +commander@7, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^10.0.0: version "10.0.1" resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" @@ -3224,21 +3288,11 @@ commander@^5.1.0: resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - commander@^8.3.0: version "8.3.0" resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@7: - version "7.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - common-path-prefix@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" @@ -3573,7 +3627,7 @@ cytoscape-cose-bilkent@^4.1.0: dependencies: cose-base "^1.0.0" -cytoscape@^3.2.0, cytoscape@^3.28.1: +cytoscape@^3.28.1: version "3.28.1" resolved "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz" integrity sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg== @@ -3581,13 +3635,6 @@ cytoscape@^3.2.0, cytoscape@^3.28.1: heap "^0.2.6" lodash "^4.17.21" -d3-array@^3.2.0, "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3: - version "3.2.4" - resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz" - integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== - dependencies: - internmap "1 - 2" - "d3-array@1 - 2": version "2.12.1" resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz" @@ -3595,6 +3642,13 @@ d3-array@^3.2.0, "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", dependencies: internmap "^1.0.0" +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.4" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + d3-axis@3: version "3.0.0" resolved "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz" @@ -3704,16 +3758,16 @@ d3-hierarchy@3: dependencies: d3-color "1 - 3" -d3-path@^3.1.0, "d3-path@1 - 3", d3-path@3: - version "3.1.0" - resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz" - integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== - d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + d3-polygon@3: version "3.0.1" resolved "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz" @@ -3761,13 +3815,6 @@ d3-scale@4: resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== -d3-shape@^1.2.0: - version "1.3.7" - resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz" - integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== - dependencies: - d3-path "1" - d3-shape@3: version "3.2.0" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz" @@ -3775,6 +3822,13 @@ d3-shape@3: dependencies: d3-path "^3.1.0" +d3-shape@^1.2.0: + version "1.3.7" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + "d3-time-format@2 - 4", d3-time-format@4: version "4.1.0" resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz" @@ -3870,27 +3924,20 @@ debounce@^1.2.1: resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@^2.6.0: +debug@2.6.9, debug@^2.6.0: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@4: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" @@ -3971,16 +4018,16 @@ delaunator@5: dependencies: robust-predicates "^3.0.2" -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + dequal@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" @@ -4509,7 +4556,7 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" -file-loader@*, file-loader@^6.2.0: +file-loader@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== @@ -4665,6 +4712,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -4807,16 +4859,16 @@ got@^12.1.0: p-cancelable "^3.0.0" responselike "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - graceful-fs@4.2.10: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz" @@ -5128,16 +5180,6 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -5149,6 +5191,16 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" @@ -5187,13 +5239,6 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@^0.6.3, iconv-lite@0.6, iconv-lite@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -5201,6 +5246,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6, iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" @@ -5264,7 +5316,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5274,16 +5326,16 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - ini@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" @@ -5294,16 +5346,16 @@ inline-style-parser@0.2.2: resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz" integrity sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ== -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - "internmap@1 - 2": version "2.0.3" resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz" @@ -5316,16 +5368,16 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ipaddr.js@^2.0.1: - version "2.1.0" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz" - integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +ipaddr.js@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz" @@ -5501,16 +5553,16 @@ is-yarn-global@^0.4.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz" integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isarray@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -6739,7 +6791,7 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -"mime-db@>= 1.43.0 < 2": +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -6749,40 +6801,14 @@ mime-db@~1.33.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz" integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.27: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime-types@^2.1.31: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime-types@~2.1.17, mime-types@2.1.18: +mime-types@2.1.18, mime-types@~2.1.17: version "2.1.18" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz" integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== dependencies: mime-db "~1.33.0" -mime-types@~2.1.24: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime-types@~2.1.34: +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -6822,7 +6848,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@3.1.2: +minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -7210,13 +7236,6 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@^1.7.0: - version "1.9.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz" - integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== - dependencies: - isarray "0.0.1" - path-to-regexp@0.1.10: version "0.1.10" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz" @@ -7227,6 +7246,13 @@ path-to-regexp@2.2.1: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz" integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@^1.7.0: + version "1.9.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== + dependencies: + isarray "0.0.1" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -7545,7 +7571,7 @@ postcss-zindex@^6.0.2: resolved "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz" integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== -"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.2, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.31, postcss@^8.4.33, postcss@^8.4.38: +postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.33, postcss@^8.4.38: version "8.4.38" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -7668,21 +7694,16 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - range-parser@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + raw-body@2.5.2: version "2.5.2" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" @@ -7733,7 +7754,7 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@*, "react-dom@^16.14.0 || 17 || ^18", "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", react-dom@^18.0.0, react-dom@^18.3.1, "react-dom@>= 16.8.0 < 19.0.0": +react-dom@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -7779,7 +7800,7 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" -react-loadable@*, "react-loadable@npm:@docusaurus/react-loadable@6.0.0": +"react-loadable@npm:@docusaurus/react-loadable@6.0.0": version "6.0.0" resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz" integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== @@ -7806,7 +7827,7 @@ react-router-dom@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@^5.3.4, react-router@>=5, react-router@5.3.4: +react-router@5.3.4, react-router@^5.3.4: version "5.3.4" resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== @@ -7821,7 +7842,7 @@ react-router@^5.3.4, react-router@>=5, react-router@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react@*, "react@^16.13.1 || ^17.0.0 || ^18.0.0", "react@^16.14.0 || ^17 || ^18", "react@^16.6.0 || ^17.0.0 || ^18.0.0", react@^18.0.0, react@^18.3.1, "react@>= 16.8.0 < 19.0.0", react@>=15, react@>=16, react@>=16.0.0: +react@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -8141,20 +8162,15 @@ sade@^1.7.3: dependencies: mri "^1.1.0" -safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" @@ -8173,25 +8189,16 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -schema-utils@^3.0.0: - version "3.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.1: - version "3.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" -schema-utils@^3.2.0: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -8210,20 +8217,6 @@ schema-utils@^4.0.0, schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -"search-insights@>= 1 < 3": - version "2.17.2" - resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.17.2.tgz" - integrity sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g== - section-matter@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz" @@ -8478,7 +8471,7 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0: +source-map@^0.6.0, source-map@~0.6.0: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -8488,11 +8481,6 @@ source-map@^0.7.0: resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" @@ -8531,45 +8519,22 @@ srcset@^4.0.0: resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz" integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + std-env@^3.0.1: version "3.7.0" resolved "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -string-width@^4.1.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8587,6 +8552,20 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringify-entities@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz" @@ -8827,11 +8806,6 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -"typescript@>= 2.7", typescript@>=4.9.5: - version "5.6.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz" - integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== - undici-types@~5.26.4: version "5.26.5" resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" @@ -8955,7 +8929,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -9184,7 +9158,7 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.88.1, "webpack@>= 4", "webpack@>=4.41.1 || 5.x", webpack@>=5, "webpack@3 || 4 || 5": +webpack@^5.88.1: version "5.94.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz" integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== @@ -9223,7 +9197,7 @@ webpackbar@^5.0.2: pretty-time "^1.1.0" std-env "^3.0.1" -websocket-driver@^0.7.4, websocket-driver@>=0.5.1: +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== From 0e766f58c93e1754a70cc1ebdbe22d85664b01ad Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Sep 2024 12:18:26 +1000 Subject: [PATCH 06/87] fix etag clearing --- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 2 +- libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs | 2 +- .../src/core/Index/Tsavorite/Implementation/InternalUpsert.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2d7f5a7a20..71c9883587 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -167,7 +167,7 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va break; default: value.UnmarkExtraMetadata(); - recordInfo.ETag = false; + recordInfo.ClearHasETag(); if (*inputPtr >= CustomCommandManager.StartOffset) { var functions = functionsState.customCommands[*inputPtr - CustomCommandManager.StartOffset].functions; diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs index a98ef6d913..8606e0596f 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs @@ -283,7 +283,7 @@ public bool ETag } public void SetHasETag() => word |= kETagBitMask; - internal void ClearHasETag() => word &= ~kETagBitMask; + public void ClearHasETag() => word &= ~kETagBitMask; public override readonly string ToString() { diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs index 1da6263d3a..353cd003fb 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs @@ -75,7 +75,7 @@ internal OperationStatus InternalUpsert= hlogBase.ReadOnlyAddress) { srcRecordInfo = ref stackCtx.recSrc.GetInfo(); - srcRecordInfo.ETag = false; + srcRecordInfo.ClearHasETag(); // Mutable Region: Update the record in-place. We perform mutable updates only if we are in normal processing phase of checkpointing UpsertInfo upsertInfo = new() From 1f7226d908dadc8c3aa38f11d342c13e5df30049 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Sep 2024 12:27:05 +1000 Subject: [PATCH 07/87] Format --- libs/server/API/GarnetStatus.cs | 2 +- libs/server/Resp/BasicCommands.cs | 11 +-- .../Functions/MainStore/PrivateMethods.cs | 2 +- .../Storage/Functions/MainStore/RMWMethods.cs | 8 +- .../Functions/MainStore/ReadMethods.cs | 8 +- test/Garnet.test/RespEtagTests.cs | 82 +++++++++---------- 6 files changed, 57 insertions(+), 56 deletions(-) diff --git a/libs/server/API/GarnetStatus.cs b/libs/server/API/GarnetStatus.cs index 54d038c4ec..3e9c26eb02 100644 --- a/libs/server/API/GarnetStatus.cs +++ b/libs/server/API/GarnetStatus.cs @@ -23,7 +23,7 @@ public enum GarnetStatus : byte /// /// Wrong type /// - WRONGTYPE, + WRONGTYPE, /// /// ETAG mismatch result for an etag based command /// diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ac8d7c6196..cc318234e8 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -301,14 +301,14 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) case GarnetStatus.WRONGTYPE: while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) SendAndReset(); - break; + break; default: if (!output.IsSpanByte) SendAndReset(output.Memory, output.Length); else dcurr += output.Length; break; - } + } return true; } @@ -345,11 +345,11 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) Debug.Assert(output.IsSpanByte); while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); - break; + break; case GarnetStatus.WRONGTYPE: while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) SendAndReset(); - break; + break; default: if (!output.IsSpanByte) SendAndReset(output.Memory, output.Length); @@ -883,7 +883,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); - } else + } + else { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index f418843a52..b5bfc5da49 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -426,7 +426,7 @@ static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory out { // Check if value contains a valid number int valLen = value.LengthWithoutMetadata - valueOffset; - byte* valPtr = value.ToPointer() + valueOffset; + byte* valPtr = value.ToPointer() + valueOffset; if (!IsValidNumber(valLen, valPtr, output.SpanByte.AsSpan(), out var val)) return true; diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 71c9883587..e3d68ca705 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -510,7 +510,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span // Copy initial etag to output CopyRespNumber(0, ref output); // early return since initial etag setting does not need to be incremented - return true;; + return true; case RespCommand.APPEND: // If nothing to append, can avoid copy update. @@ -568,7 +568,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span output.Length = outp.Length; if (!ret) return false; - + break; } throw new GarnetException("Unsupported operation on input"); @@ -701,7 +701,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.SET: case RespCommand.SETEXXX: // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX - Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length -etagIgnoredOffset); + Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length - etagIgnoredOffset); // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) @@ -719,7 +719,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: Debug.Assert(oldValue.MetadataSize + input.Length - RespInputHeader.Size == newValue.Length); - + // Check if SetGet flag is set if (((RespInputHeader*)inputPtr)->CheckSetGetFlag()) { diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index a67093828e..ab6567f0e6 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -26,7 +26,7 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu if (isEtagCmd && !readInfo.RecordInfo.ETag) { // Used to indicate wrong type operation - readInfo.Action = ReadAction.CancelOperation; + readInfo.Action = ReadAction.CancelOperation; return false; } @@ -52,7 +52,7 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu return true; } } - + // Unless the command explicitly asks for the ETag in response, we do not write back the ETag var start = 0; var end = -1; @@ -81,14 +81,14 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte } var cmd = ((RespInputHeader*)input.ToPointer())->cmd; - + var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; // ETAG Read command on non-ETag data should early exit but indicate the wrong type if (isEtagCmd && !recordInfo.ETag) { // Used to indicate wrong type operation - readInfo.Action = ReadAction.CancelOperation; + readInfo.Action = ReadAction.CancelOperation; return false; } diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 44a396f78b..a6e6850ed6 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -102,7 +102,7 @@ public void GetWithEtagReturnsValAndEtagForKey() var _ = db.Execute("SETWITHETAG", [key, "hkhalid"]); RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", [key]); - long etag= long.Parse(res[0].ToString()); + long etag = long.Parse(res[0].ToString()); string value = res[1].ToString(); ClassicAssert.AreEqual(0, etag); @@ -128,7 +128,7 @@ public void GetIfNotMatchReturnsDataWhenEtagDoesNotMatch() ClassicAssert.AreEqual("NOTCHANGED", noDataOnMatch.ToString()); RedisResult[] res = (RedisResult[])db.Execute("GETIFNOTMATCH", [key, 1]); - long etag= long.Parse(res[0].ToString()); + long etag = long.Parse(res[0].ToString()); string value = res[1].ToString(); ClassicAssert.AreEqual(0, etag); @@ -161,7 +161,7 @@ public void SetWithEtagOnAlreadyExistingDataOverridesIt() etag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, etag); } - + [Test] public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesIt() { @@ -170,7 +170,7 @@ public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesIt() RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); long etag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, etag); - + // inplace update res = db.Execute("SETWITHETAG", ["rizz", "meow"]); etag = long.Parse(res.ToString()); @@ -288,10 +288,10 @@ public async Task LargeEtagSetGet() ClassicAssert.AreEqual(0, initalEtag); // Backwards compatability of data set with etag and plain GET call - var retvalue = (byte[]) await db.StringGetAsync("mykey"); + var retvalue = (byte[])await db.StringGetAsync("mykey"); ClassicAssert.IsTrue(new ReadOnlySpan(value).SequenceEqual(new ReadOnlySpan(retvalue))); - } + } [Test] public void SetExpiryForEtagSetData() @@ -385,7 +385,7 @@ public void SetGetForEtagSetData() ClassicAssert.AreEqual(newValue1, retValue); // This should increase the ETAG internally so we have a check for that here - long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); ClassicAssert.AreEqual(1, checkEtag); // Smaller new value with KeepTtl @@ -393,7 +393,7 @@ public void SetGetForEtagSetData() retValue = db.StringSetAndGet(key, newValue2, null, true, When.Always, CommandFlags.None); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); ClassicAssert.AreEqual(2, checkEtag); ClassicAssert.AreEqual(newValue1, retValue); @@ -406,10 +406,10 @@ public void SetGetForEtagSetData() string newValue3 = "01234"; retValue = db.StringSetAndGet(key, newValue3, TimeSpan.FromSeconds(10), When.Exists, CommandFlags.None); ClassicAssert.AreEqual(newValue2, retValue); - + // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); - ClassicAssert.AreEqual(3, checkEtag); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); + ClassicAssert.AreEqual(3, checkEtag); retValue = db.StringGet(key); ClassicAssert.AreEqual(newValue3, retValue); @@ -422,7 +422,7 @@ public void SetGetForEtagSetData() ClassicAssert.AreEqual(newValue3, retValue); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); ClassicAssert.AreEqual(4, checkEtag); retValue = db.StringGet(key); @@ -440,7 +440,7 @@ public void SetGetForEtagSetData() ClassicAssert.IsNull(expiry); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); ClassicAssert.AreEqual(5, checkEtag); // Larger new value without expiration @@ -453,7 +453,7 @@ public void SetGetForEtagSetData() ClassicAssert.IsNull(expiry); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ key ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); ClassicAssert.AreEqual(6, checkEtag); } @@ -468,13 +468,13 @@ public void SetExpiryIncrForEtagSetData() var strKey = "key1"; db.Execute("SETWITHETAG", [strKey, nVal]); db.KeyExpire(strKey, TimeSpan.FromSeconds(5)); - + string res1 = db.StringGet(strKey); long n = db.StringIncrement(strKey); - + // This should increase the ETAG internally so we have a check for that here - var checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + var checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(1, checkEtag); string res = db.StringGet(strKey); @@ -485,7 +485,7 @@ public void SetExpiryIncrForEtagSetData() n = db.StringIncrement(strKey); // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(2, checkEtag); nRetVal = Convert.ToInt64(db.StringGet(strKey)); @@ -503,7 +503,7 @@ public void SetExpiryIncrForEtagSetData() ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(1, nRetVal); - RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [ strKey ])); + RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [strKey])); ClassicAssert.IsNotNull(ex); ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); } @@ -518,7 +518,7 @@ public void IncrDecrChangeDigitsWithExpiry() db.Execute("SETWITHETAG", [strKey, 9]); - long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(0, checkEtag); db.KeyExpire(strKey, TimeSpan.FromSeconds(5)); @@ -528,7 +528,7 @@ public void IncrDecrChangeDigitsWithExpiry() ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(10, nRetVal); - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(1, checkEtag); n = db.StringDecrement(strKey); @@ -536,7 +536,7 @@ public void IncrDecrChangeDigitsWithExpiry() ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(9, nRetVal); - checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(2, checkEtag); Thread.Sleep(TimeSpan.FromSeconds(5)); @@ -554,7 +554,7 @@ public void StringSetOnAnExistingEtagDataOverrides() var strKey = "mykey"; db.Execute("SETWITHETAG", [strKey, 9]); - long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(0, checkEtag); // override the setwithetag to a new value altogether, this will make it lose it's etag capability @@ -567,7 +567,7 @@ public void StringSetOnAnExistingEtagDataOverrides() string retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociao", retVal); - RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [ strKey ])); + RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [strKey])); ClassicAssert.IsNotNull(ex); ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); } @@ -580,7 +580,7 @@ public void LockTakeReleaseOnAValueInitiallySetWithEtag() string key = "lock-key"; string value = "lock-value"; - + var initalEtag = long.Parse(db.Execute("SETWITHETAG", [key, value]).ToString()); ClassicAssert.AreEqual(0, initalEtag); @@ -628,7 +628,7 @@ public void SingleDecrForEtagSetData(string strKey, int nVal) long nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); - long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(1, checkEtag); } @@ -651,7 +651,7 @@ public void SingleDecrByForEtagSetData(long nVal, long nDecr) int nRetVal = Convert.ToInt32(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); - long checkEtag = long.Parse(db.Execute("GETWITHETAG", [ strKey ])[0].ToString()); + long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(1, checkEtag); } @@ -705,7 +705,7 @@ public void SimpleIncrementOverflowForEtagSetData(RespCommand cmd) var db = redis.GetDatabase(0); var exception = false; - var key = "test"; + var key = "test"; try { @@ -737,7 +737,7 @@ public void SimpleIncrementOverflowForEtagSetData(RespCommand cmd) } ClassicAssert.IsTrue(exception); } - + [Test] public void SingleDeleteForEtagSetData() @@ -913,7 +913,7 @@ public void SingleExistsForEtagSetData([Values] bool withoutObjectStore) var nVal = 100; var strKey = "key1"; ClassicAssert.IsFalse(db.KeyExists(strKey)); - + db.Execute("SETWITHETAG", [strKey, nVal]); bool fExists = db.KeyExists("key1", CommandFlags.None); @@ -1344,7 +1344,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.AreEqual("15", resp.ToString()); resp = db.StringGet(key); ClassicAssert.AreEqual("012340123456789", resp.ToString()); - + // should update the etag internally var updatedEtagRes = db.Execute("GETWITHETAG", key); ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); @@ -1369,7 +1369,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.AreEqual("10", resp.ToString()); resp = db.StringGet(key); ClassicAssert.AreEqual("ABCDE56789", resp.ToString()); - + // should update the etag internally updatedEtagRes = db.Execute("GETWITHETAG", key); ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); @@ -1381,7 +1381,7 @@ public void SetRangeTestForEtagSetData() resp = db.StringSetRange(key, 5, newValue); ClassicAssert.AreEqual("10", resp.ToString()); - + updatedEtagRes = db.Execute("GETWITHETAG", key); ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); @@ -1575,7 +1575,7 @@ public void AppendTestForEtagSetData() // Test appending an empty string db.Execute("SETWITHETAG", [key, val]); - + var len1 = db.StringAppend(key, ""); ClassicAssert.AreEqual(val.Length, len1); @@ -1598,10 +1598,10 @@ public void AppendTestForEtagSetData() // Test appending to a key with a large value var largeVal = new string('a', 1000000); - db.Execute("SETWITHETAG", [ key, largeVal ]); + db.Execute("SETWITHETAG", [key, largeVal]); var len3 = db.StringAppend(key, val2); ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); - + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); ClassicAssert.AreEqual(1, etagToCheck); @@ -1634,9 +1634,9 @@ public void SetBitOperationsOnEtagSetData() string key = "miki"; // 64 BIT BITMAP Byte[] initialBitmap = new byte[8]; - string bitMapAsStr = Encoding.UTF8.GetString(initialBitmap);; + string bitMapAsStr = Encoding.UTF8.GetString(initialBitmap); ; - db.Execute("SETWITHETAG", [ key, bitMapAsStr]); + db.Execute("SETWITHETAG", [key, bitMapAsStr]); long setbits = db.StringBitCount(key); ClassicAssert.AreEqual(0, setbits); @@ -1702,7 +1702,7 @@ public void BitFieldSetGetOnEtagSetData() long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); ClassicAssert.AreEqual(1, etagToCheck); - + // Get value back var getResult = (RedisResult[])db.Execute("BITFIELD", key, "GET", "u8", "0"); @@ -1720,7 +1720,7 @@ public void BitFieldIncrementWithWrapOverflowOnEtagSetData() // Arrange - Set an 8-bit unsigned value at offset 0 db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte - + // Act - Set initial value to 255 and try to increment by 1 db.Execute("BITFIELD", key, "SET", "u8", "0", "255"); long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); @@ -1745,7 +1745,7 @@ public void BitFieldIncrementWithSaturateOverflowOnEtagSetData() // Arrange - Set an 8-bit unsigned value at offset 0 db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte - + // Act - Set initial value to 250 and try to increment by 10 with saturate overflow db.Execute("BITFIELD", key, "SET", "u8", "0", "250"); From c7cb0b5f38265110c48ff84749071ce24da5a39a Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Sep 2024 12:39:39 +1000 Subject: [PATCH 08/87] fix build warnings --- libs/storage/Tsavorite/cs/test/ExpirationTests.cs | 2 +- libs/storage/Tsavorite/cs/test/RevivificationTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/storage/Tsavorite/cs/test/ExpirationTests.cs b/libs/storage/Tsavorite/cs/test/ExpirationTests.cs index d47f5047fd..dc317a865a 100644 --- a/libs/storage/Tsavorite/cs/test/ExpirationTests.cs +++ b/libs/storage/Tsavorite/cs/test/ExpirationTests.cs @@ -470,7 +470,7 @@ public override void ReadCompletionCallback(ref SpanByte key, ref ExpirationInpu } /// - public override int GetRMWModifiedValueLength(ref SpanByte value, ref ExpirationInput input) => value.TotalSize; + public override int GetRMWModifiedValueLength(ref SpanByte value, ref ExpirationInput input, bool hasEtag) => value.TotalSize; /// public override int GetRMWInitialValueLength(ref ExpirationInput input) => MinValueLen; diff --git a/libs/storage/Tsavorite/cs/test/RevivificationTests.cs b/libs/storage/Tsavorite/cs/test/RevivificationTests.cs index 4ec87cdb82..d10cd125bc 100644 --- a/libs/storage/Tsavorite/cs/test/RevivificationTests.cs +++ b/libs/storage/Tsavorite/cs/test/RevivificationTests.cs @@ -576,7 +576,7 @@ public override bool InPlaceUpdater(ref SpanByte key, ref SpanByte input, ref Sp } // Override the default SpanByteFunctions impelementation; for these tests, we always want the input length. - public override int GetRMWModifiedValueLength(ref SpanByte value, ref SpanByte input) => input.TotalSize; + public override int GetRMWModifiedValueLength(ref SpanByte value, ref SpanByte input, bool hasEtag) => input.TotalSize; public override bool SingleDeleter(ref SpanByte key, ref SpanByte value, ref DeleteInfo deleteInfo, ref RecordInfo recordInfo) { From 3631d20c0f42df517704e9e36f770351fd368f7f Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Sep 2024 17:36:35 +1000 Subject: [PATCH 09/87] Update test --- libs/server/Resp/RespCommandsInfo.json | 56 +++++++++++++++++ .../CommandInfoUpdater/SupportedCommand.cs | 4 ++ test/Garnet.test/Resp/ACL/RespCommandTests.cs | 60 +++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/libs/server/Resp/RespCommandsInfo.json b/libs/server/Resp/RespCommandsInfo.json index aed450d1d1..af52239622 100644 --- a/libs/server/Resp/RespCommandsInfo.json +++ b/libs/server/Resp/RespCommandsInfo.json @@ -1888,6 +1888,20 @@ ], "SubCommands": null }, + { + "Command": "GETIFNOTMATCH", + "Name": "GETIFNOTMATCH", + "IsInternal": false, + "Arity": 2, + "Flags": "NONE", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Read", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, { "Command": "GETRANGE", "Name": "GETRANGE", @@ -1917,6 +1931,20 @@ ], "SubCommands": null }, + { + "Command": "GETWITHETAG", + "Name": "GETWITHETAG", + "IsInternal": false, + "Arity": 1, + "Flags": "NONE", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Read", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, { "Command": "HDEL", "Name": "HDEL", @@ -4109,6 +4137,20 @@ ], "SubCommands": null }, + { + "Command": "SETIFMATCH", + "Name": "SETIFMATCH", + "IsInternal": false, + "Arity": 3, + "Flags": "NONE", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, { "Command": "SETRANGE", "Name": "SETRANGE", @@ -4138,6 +4180,20 @@ ], "SubCommands": null }, + { + "Command": "SETWITHETAG", + "Name": "SETWITHETAG", + "IsInternal": false, + "Arity": 2, + "Flags": "NONE", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, { "Command": "SINTER", "Name": "SINTER", diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 1e00233072..5f506a6433 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -123,7 +123,9 @@ public class SupportedCommand new("GET", RespCommand.GET), new("GETBIT", RespCommand.GETBIT), new("GETDEL", RespCommand.GETDEL), + new("GETIFNOTMATCH", RespCommand.GETIFNOTMATCH), new("GETRANGE", RespCommand.GETRANGE), + new("GETWITHETAG", RespCommand.GETWITHETAG), new("HDEL", RespCommand.HDEL), new("HELLO", RespCommand.HELLO), new("HEXISTS", RespCommand.HEXISTS), @@ -214,7 +216,9 @@ public class SupportedCommand new("SET", RespCommand.SET), new("SETBIT", RespCommand.SETBIT), new("SETEX", RespCommand.SETEX), + new("SETIFMATCH", RespCommand.SETIFMATCH), new("SETRANGE", RespCommand.SETRANGE), + new("SETWITHETAG", RespCommand.SETWITHETAG), new("SISMEMBER", RespCommand.SISMEMBER), new("SLAVEOF", RespCommand.SECONDARYOF), new("SMEMBERS", RespCommand.SMEMBERS), diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 8c755b0074..4d496ba5d6 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -4631,6 +4631,66 @@ static async Task DoSetKeepTtlXxAsync(GarnetClient client) } } + [Test] + public async Task SetWithEtagACLsAsync() + { + await CheckCommandsAsync( + "SETWITHETAG", + [DoSetWithEtagAsync] + ); + + static async Task DoSetWithEtagAsync(GarnetClient client) + { + long val = await client.ExecuteForLongResultAsync("SETWITHETAG", ["foo", "bar"]); + ClassicAssert.AreEqual(0, val); + } + } + + [Test] + public async Task SetIfMatchACLsAsync() + { + await CheckCommandsAsync( + "SETIFMATCH", + [DoSetIfMatchAsync] + ); + + static async Task DoSetIfMatchAsync(GarnetClient client) + { + var res = await client.ExecuteForStringResultAsync("SETIFMATCH", ["foo", "rizz", "0"]); + ClassicAssert.IsNull(res); + } + } + + [Test] + public async Task GetIfNotMatchACLsAsync() + { + await CheckCommandsAsync( + "GETIFNOTMATCH", + [DoGetIfNotMatchAsync] + ); + + static async Task DoGetIfNotMatchAsync(GarnetClient client) + { + var res = await client.ExecuteForStringResultAsync("GETIFNOTMATCH", ["foo", "0"]); + ClassicAssert.IsNull(res); + } + } + + [Test] + public async Task GetWithEtagACLsAsync() + { + await CheckCommandsAsync( + "GETWITHETAG", + [DoGetWithEtagAsync] + ); + + static async Task DoGetWithEtagAsync(GarnetClient client) + { + var res = await client.ExecuteForStringResultAsync("GETWITHETAG", ["foo"]); + ClassicAssert.IsNull(res); + } + } + [Test] public async Task SetBitACLsAsync() { From a079c0f83300087e0deac35e3dd949b7b7628ffe Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 30 Sep 2024 18:22:30 +1000 Subject: [PATCH 10/87] Update tests for badrish reqs --- libs/server/InputHeader.cs | 18 ++ libs/server/Resp/BasicCommands.cs | 119 +++++++-- libs/server/Resp/CmdStrings.cs | 3 +- .../Functions/MainStore/PrivateMethods.cs | 43 +++- .../Storage/Functions/MainStore/RMWMethods.cs | 77 ++++-- .../Functions/MainStore/ReadMethods.cs | 62 ++--- .../Functions/MainStore/VarLenInputMethods.cs | 6 + test/Garnet.test/RespEtagTests.cs | 233 ++++++++++++++---- website/docs/commands/etag-commands.md | 19 +- website/docs/commands/raw-string.md | 3 +- 10 files changed, 433 insertions(+), 150 deletions(-) diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index 67a19fbbe4..4736fe0a57 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -26,6 +26,13 @@ enum RespInputFlags : byte /// Expired /// Expired = 128, + + /// + /// Flag indicating if a SET operation should retain the etag of the previous value if it exists. + /// This is used for conditional setting. + /// + RetainEtag = 129, + } /// @@ -100,6 +107,17 @@ internal ListOperation ListOp /// internal unsafe void SetSetGetFlag() => flags |= RespInputFlags.SetGet; + /// + /// Set "RetainEtag" flag, used to update the old etag of a key after conditionally setting it + /// + internal unsafe void SetRetainEtagFlag() => flags |= RespInputFlags.RetainEtag; + + /// + /// Check if the RetainEtagFlag is set + /// + /// + internal unsafe bool CheckRetainEtagFlag() => (flags & RespInputFlags.RetainEtag) != 0; + /// /// Check if record is expired, either deterministically during log replay, /// or based on current time in normal operation. diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index cc318234e8..dea767ddda 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -290,7 +290,6 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) var status = storageApi.GETForETagCmd(ref key, ref input, ref output); - // Get the ETAG and Value and start typing switch (status) { case GarnetStatus.NOTFOUND: @@ -400,7 +399,8 @@ P.s. This is NOT GOING TO create a buffer overflow becuase of the following reas // Set key length *(int*)keyPtr = key.Length; - NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, ref storageApi); + // Here Etag retain argument does not really matter because setifmatch may or may not update etag based on the "if match" condition + NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, true, ref storageApi); // restore the 8 bytes we had messed with on the network buffer *(long*)borrowedMemLocation = saved8Bytes; @@ -409,24 +409,52 @@ P.s. This is NOT GOING TO create a buffer overflow becuase of the following reas } /// - /// SETWITHETAG key val + /// SETWITHETAG key val [RETAINETAG] /// Sets a key value pair with an ETAG associated with the value internally /// Calling this on a key that already exists is an error case /// private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - Debug.Assert(parseState.Count == 2); + Debug.Assert(parseState.Count == 2 || parseState.Count == 3); var key = parseState.GetArgSliceByRef(0).SpanByte; var value = parseState.GetArgSliceByRef(1).SpanByte; + bool retainEtag = false; + if (parseState.Count == 3) + { + Span opt = parseState.GetArgSliceByRef(2).Span; + if (opt.SequenceEqual(CmdStrings.RETAINETAG)) + { + retainEtag = true; + } + else + { + AsciiUtils.ToUpperInPlace(opt); + if (opt.SequenceEqual(CmdStrings.RETAINETAG)) + { + retainEtag = true; + } + else + { + // Unknown option + while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) + SendAndReset(); + return true; + } + + } + } + + // Make space for key header var keyPtr = key.ToPointer() - sizeof(int); // Set key length *(int*)keyPtr = key.Length; - NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, ref storageApi); + // calling set with etag on an exisitng key will update the etag of the existing key + NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, retainEtag, ref storageApi); return true; } @@ -554,6 +582,12 @@ private bool NetworkSETEX(bool highPrecision, ref TGarnetApi storage return true; } + enum EtagRetentionOption : byte + { + None, + RETAIN, + } + enum ExpirationOption : byte { None, @@ -587,6 +621,7 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) ReadOnlySpan errorMessage = default; var existOptions = ExistOptions.None; var expOption = ExpirationOption.None; + var retainEtagOption = EtagRetentionOption.None; var getValue = false; var tokenIdx = 2; @@ -673,9 +708,18 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) } else if (nextOpt.SequenceEqual(CmdStrings.GET)) { - tokenIdx++; + // tokenIdx++; // HK TODO: WHY TAL WHYYYY!????? getValue = true; } + else if (nextOpt.SequenceEqual(CmdStrings.RETAINETAG)) + { + if (retainEtagOption != EtagRetentionOption.None) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; + break; + } + retainEtagOption = EtagRetentionOption.RETAIN; + } else { if (!optUpperCased) @@ -709,6 +753,8 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) var valPtr = sbVal.ToPointer() - sizeof(int); var vSize = sbVal.Length; + bool isEtagRetained = retainEtagOption == EtagRetentionOption.RETAIN; + switch (expOption) { case ExpirationOption.None: @@ -716,17 +762,26 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) switch (existOptions) { case ExistOptions.None: - return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, - false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, false, - ref storageApi); // Can perform a blind update + if (isEtagRetained) + { + // cannot do blind upsert if isEtagRetained + return NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, getValue, + false, true, ref storageApi); + } + else + { + return getValue + ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + false, false, ref storageApi) + : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, false, + ref storageApi); // Can perform a blind update + } case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, keyPtr, valPtr, vSize, - getValue, false, ref storageApi); + getValue, isEtagRetained, false, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, - getValue, false, ref storageApi); + getValue, false, isEtagRetained, ref storageApi); } break; @@ -734,17 +789,26 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) switch (existOptions) { case ExistOptions.None: - return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, - true, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, - ref storageApi); // Can perform a blind update + if (isEtagRetained) + { + // cannot do a blind update + return NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, getValue, + true, true, ref storageApi); + } + else + { + return getValue + ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + true, false, ref storageApi) + : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + ref storageApi); // Can perform a blind update + } case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, keyPtr, valPtr, vSize, - getValue, true, ref storageApi); + getValue, true, isEtagRetained, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, - getValue, true, ref storageApi); + getValue, true, isEtagRetained, ref storageApi); } break; @@ -756,13 +820,13 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) case ExistOptions.None: // We can never perform a blind update due to KEEPTTL return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, keyPtr, valPtr, vSize, - getValue, false, ref storageApi); + getValue, false, isEtagRetained, ref storageApi); case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, keyPtr, valPtr, vSize, - getValue, false, ref storageApi); + getValue, false, isEtagRetained, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, - getValue, false, ref storageApi); + getValue, false, isEtagRetained, ref storageApi); } break; @@ -801,7 +865,7 @@ private bool NetworkSET_EX(RespCommand cmd, int expiry, byte* keyPtr } private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byte* keyPtr, - byte* inputPtr, int isize, bool getValue, bool highPrecision, ref TGarnetApi storageApi) + byte* inputPtr, int isize, bool getValue, bool highPrecision, bool retainEtag, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { // Make space for RespCommand in input @@ -814,6 +878,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt ((RespInputHeader*)(inputPtr + sizeof(int)))->flags = 0; if (getValue) ((RespInputHeader*)(inputPtr + sizeof(int)))->SetSetGetFlag(); + if (retainEtag) + ((RespInputHeader*)(inputPtr + sizeof(int)))->SetRetainEtagFlag(); } else { @@ -825,6 +891,9 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt ((RespInputHeader*)(inputPtr + sizeof(int) + sizeof(long)))->flags = 0; if (getValue) ((RespInputHeader*)(inputPtr + sizeof(int) + sizeof(long)))->SetSetGetFlag(); + if (retainEtag) + ((RespInputHeader*)(inputPtr + sizeof(int) + sizeof(long)))->SetRetainEtagFlag(); + SpanByte.Reinterpret(inputPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + (highPrecision ? TimeSpan.FromMilliseconds(expiry).Ticks @@ -852,7 +921,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt SendAndReset(); break; case GarnetStatus.ETAGMISMATCH: - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) SendAndReset(); break; case GarnetStatus.WRONGTYPE: diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index fd47741c65..76f2892423 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -93,6 +93,7 @@ static partial class CmdStrings public static ReadOnlySpan KEEPTTL => "KEEPTTL"u8; public static ReadOnlySpan NX => "NX"u8; public static ReadOnlySpan XX => "XX"u8; + public static ReadOnlySpan RETAINETAG => "RETAINETAG"u8; public static ReadOnlySpan UNSAFETRUNCATELOG => "UNSAFETRUNCATELOG"u8; public static ReadOnlySpan SAMPLES => "SAMPLES"u8; public static ReadOnlySpan RANK => "RANK"u8; @@ -115,6 +116,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_EMPTY => "$0\r\n\r\n"u8; public static ReadOnlySpan RESP_QUEUED => "+QUEUED\r\n"u8; public static ReadOnlySpan RESP_VALNOTCHANGED => "+NOTCHANGED\r\n"u8; + public static ReadOnlySpan RESP_ETAGMISMTACH => "+ETAGMISMATCH\r\n"u8; /// /// Simple error response strings, i.e. these are of the form "-errorString\r\n" @@ -187,7 +189,6 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_XX_NX_NOT_COMPATIBLE => "ERR XX and NX options at the same time are not compatible"u8; public static ReadOnlySpan RESP_ERR_GT_LT_NX_NOT_COMPATIBLE => "ERR GT, LT, and/or NX options at the same time are not compatible"u8; public static ReadOnlySpan RESP_ERR_INCR_SUPPORTS_ONLY_SINGLE_PAIR => "ERR INCR option supports a single increment-element pair"u8; - public static ReadOnlySpan RESP_ETAGMISMTACH => "ETAGMISMATCH Given ETag does not match existing ETag."u8; /// /// Response string templates diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index b5bfc5da49..5fcffd024a 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -82,7 +82,7 @@ void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int } } - void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending, int payloadEtagEnd, int etagIgnoredDataEnd) + void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending, int payloadEtagEnd, int etagIgnoredDataEnd, bool hasEtagInVal) { var inputPtr = input.ToPointer(); switch ((RespCommand)(*inputPtr)) @@ -217,10 +217,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd (start, end) = NormalizeRange(start, end, len); CopyRespTo(ref value, ref dst, start + payloadEtagEnd, end + payloadEtagEnd); return; - case RespCommand.GETIFNOTMATCH: case RespCommand.SETIFMATCH: - case RespCommand.GETWITHETAG: - // Get value without RESP header; exclude expiration // extract ETAG, write as long into dst, and then value long etag = *(long*)value.ToPointer(); // remove the length of the ETAG @@ -231,6 +228,35 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd int desiredLength = 4 + 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); return; + + case RespCommand.GETIFNOTMATCH: + case RespCommand.GETWITHETAG: + // If this has an etag then we want to use it other wise null + // we know somethgin doesnt have an etag if + etag = -1; + valueLength = value.LengthWithoutMetadata; + + if (hasEtagInVal) + { + // Get value without RESP header; exclude expiration + // extract ETAG, write as long into dst, and then value + etag = *(long*)value.ToPointer(); + // remove the length of the ETAG + valueLength -= sizeof(long); + // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below + etagTruncatedVal = value.AsReadOnlySpan(sizeof(long)); + // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n + desiredLength = 4 + 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; + } + else + { + etagTruncatedVal = value.AsReadOnlySpan(); + // instead of :(etagdigits) we will have nil after array len + desiredLength = 4 + 3 + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; + } + + WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); + return; default: throw new GarnetException("Unsupported operation on input"); } @@ -263,7 +289,14 @@ static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref b // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element var initPtr = curr; RespWriteUtils.WriteArrayLength(2, ref curr, end); - RespWriteUtils.WriteInteger(etag, ref curr, end); + if (etag == -1) + { + RespWriteUtils.WriteNull(ref curr, end); + } + else + { + RespWriteUtils.WriteInteger(etag, ref curr, end); + } RespWriteUtils.WriteBulkString(value, ref curr, end); } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index e3d68ca705..7717e5e02b 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -267,8 +267,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span return true; case RespCommand.SETIFMATCH: + // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH + // In this case no etag will match the "nil" etag on a record without an etag if (!recordInfo.ETag) { + rmwInfo.Action = RMWAction.CancelOperation; return false; } @@ -297,12 +300,22 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan()); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1); + CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1, true); // early return since we already updated the ETag return true; case RespCommand.SET: case RespCommand.SETEXXX: + var nextUpdateEtagOffset = etagIgnoredOffset; + var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) + { + // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + recordInfo.ClearHasETag(); + } + // Need CU if no space for new value if (input.Length - RespInputHeader.Size > value.Length - etagIgnoredOffset) return false; @@ -316,17 +329,26 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + etagIgnoredOffset); + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + nextUpdateEtagOffset); // Copy input to value value.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(etagIgnoredOffset)); + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(nextUpdateEtagOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: + // respect etag retention only if input header tells you to explicitly + if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) + { + // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record + etagIgnoredOffset = 0; + etagIgnoredEnd = -1; + recordInfo.ClearHasETag(); + } + // Need CU if no space for new value if (value.MetadataSize + input.Length - RespInputHeader.Size > value.Length - etagIgnoredOffset) return false; @@ -497,18 +519,20 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span if (input.Length - RespInputHeader.Size + sizeof(long) > value.Length) return false; + // retain the older etag (and increment it to account for this update) if requested and if it also exists otherwise set etag to initial etag of 0 + long etagVal = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag() && recordInfo.ETag ? (oldEtag + 1) : 0; + recordInfo.SetHasETag(); // Copy input to value value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + sizeof(long)); value.ExtraMetadata = input.ExtraMetadata; - // initial etag set to 0, this is a counter based etag that is incremented on change - *(long*)value.ToPointer() = 0; + *(long*)value.ToPointer() = etagVal; input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); // Copy initial etag to output - CopyRespNumber(0, ref output); + CopyRespNumber(etagVal, ref output); // early return since initial etag setting does not need to be incremented return true; @@ -587,8 +611,6 @@ public bool NeedCopyUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte ol { var inputPtr = input.ToPointer(); - var cmd = (RespCommand)(*inputPtr); - int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; if (rmwInfo.RecordInfo.ETag) @@ -672,19 +694,23 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa { case RespCommand.SETWITHETAG: Debug.Assert(input.Length - RespInputHeader.Size + sizeof(long) == newValue.Length); - // initial etag setting so does not need to be incremented + + // etag setting will be done here so does not need to be incremented outside switch shouldUpdateEtag = false; + // retain the older etag (and increment it to account for this update) if requested and if it also exists otherwise set etag to initial etag of 0 + long etagVal = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag() && recordInfo.ETag ? (oldEtag + 1) : 0; recordInfo.SetHasETag(); // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(sizeof(long))); - // initial Etag - *(long*)newValue.ToPointer() = 0; + // set the etag + *(long*)newValue.ToPointer() = etagVal; // Copy initial etag to output - CopyRespNumber(0, ref output); + CopyRespNumber(etagVal, ref output); break; + case RespCommand.SETIFMATCH: - Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data."); + Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data. Inplace update should have returned mismatch."); // this update is so the early call to send the resp command works, outside of the switch // we are doing a double op of setting the etag to normalize etag update for other operations @@ -695,11 +721,21 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan()); // Write Etag and Val back to Client - CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1); + CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1, true); break; case RespCommand.SET: case RespCommand.SETEXXX: + var nextUpdateEtagOffset = etagIgnoredOffset; + var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) + { + // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + recordInfo.ClearHasETag(); + } + // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length - etagIgnoredOffset); @@ -713,11 +749,20 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(etagIgnoredOffset)); + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: + nextUpdateEtagOffset = etagIgnoredOffset; + nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) + { + // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + } + Debug.Assert(oldValue.MetadataSize + input.Length - RespInputHeader.Size == newValue.Length); // Check if SetGet flag is set @@ -729,7 +774,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa // Copy input to value, retain metadata of oldValue newValue.ExtraMetadata = oldValue.ExtraMetadata; - input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(newValue.AsSpan(etagIgnoredOffset)); + input.AsReadOnlySpan().Slice(RespInputHeader.Size).CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); break; case RespCommand.EXPIRE: diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index ab6567f0e6..90a7a96b73 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -22,15 +22,18 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; - // ETAG Read command on non-ETag data should early exit but indicate the wrong type - if (isEtagCmd && !readInfo.RecordInfo.ETag) + if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { - // Used to indicate wrong type operation - readInfo.Action = ReadAction.CancelOperation; - return false; + long existingEtag = *(long*)value.ToPointer(); + long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag == etagToMatchAgainst) + { + // write the value not changed message to dst, and early return + CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + return true; + } } - - if ((byte)cmd >= CustomCommandManager.StartOffset) + else if ((byte)cmd >= CustomCommandManager.StartOffset) { int valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) outp = (dst.Memory, 0); @@ -41,18 +44,6 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu return ret; } - if (cmd == RespCommand.GETIFNOTMATCH) - { - long existingEtag = *(long*)value.ToPointer(); - long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); - if (existingEtag == etagToMatchAgainst) - { - // write the value not changed message to dst, and early return - CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); - return true; - } - } - // Unless the command explicitly asks for the ETag in response, we do not write back the ETag var start = 0; var end = -1; @@ -65,7 +56,7 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu if (input.Length == 0) CopyRespTo(ref value, ref dst, start, end); else - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, readInfo.RecordInfo.ETag); return true; } @@ -84,15 +75,18 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; - // ETAG Read command on non-ETag data should early exit but indicate the wrong type - if (isEtagCmd && !recordInfo.ETag) + if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { - // Used to indicate wrong type operation - readInfo.Action = ReadAction.CancelOperation; - return false; + long existingEtag = *(long*)value.ToPointer(); + long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag == etagToMatchAgainst) + { + // write the value not changed message to dst, and early return + CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + return true; + } } - - if ((byte)cmd >= CustomCommandManager.StartOffset) + else if ((byte)cmd >= CustomCommandManager.StartOffset) { int valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) outp = (dst.Memory, 0); @@ -103,18 +97,6 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte return ret; } - if (cmd == RespCommand.GETIFNOTMATCH) - { - long existingEtag = *(long*)value.ToPointer(); - long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); - if (existingEtag == etagToMatchAgainst) - { - // write the value not changed message to dst, and early return - CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); - return true; - } - } - // Unless the command explicitly asks for the ETag in response, we do not write back the ETag var start = 0; var end = -1; @@ -128,7 +110,7 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte CopyRespTo(ref value, ref dst, start, end); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, recordInfo.ETag); } return true; diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index a177bd5019..29c1377b2e 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -113,6 +113,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool ha var inputPtr = input.ToPointer(); var cmd = inputspan[0]; int etagOffset = hasEtag ? 8 : 0; + bool retainEtag = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag(); + switch ((RespCommand)cmd) { case RespCommand.INCR: @@ -165,10 +167,14 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool ha case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: + if (!retainEtag) + etagOffset = 0; return sizeof(int) + t.MetadataSize + input.Length - RespInputHeader.Size + etagOffset; case RespCommand.SET: case RespCommand.SETEXXX: + if (!retainEtag) + etagOffset = 0; return sizeof(int) + input.Length - RespInputHeader.Size + etagOffset; case RespCommand.SETIFMATCH: case RespCommand.PERSIST: diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index a6e6850ed6..270a74f72a 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -57,6 +57,11 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() long initalEtag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, initalEtag); + // ETAGMISMATCH test + var incorrectEtag = 1738; + RedisResult etagMismatchMsg = db.Execute("SETIFMATCH", [key, "nextone", incorrectEtag]); + ClassicAssert.AreEqual("ETAGMISMATCH", etagMismatchMsg.ToString()); + // set a bigger val RedisResult[] setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", initalEtag]); @@ -74,6 +79,10 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() ClassicAssert.AreEqual(2, nextEtag); ClassicAssert.AreEqual(value, "nextnextone"); + // ETAGMISMATCH again + etagMismatchMsg = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]); + ClassicAssert.AreEqual("ETAGMISMATCH", etagMismatchMsg.ToString()); + // set a smaller val setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "lastOne", nextEtag]); nextEtag = long.Parse(setIfMatchRes[0].ToString()); @@ -99,7 +108,8 @@ public void GetWithEtagReturnsValAndEtagForKey() ClassicAssert.IsTrue(nonExistingData.IsNull); // insert data - var _ = db.Execute("SETWITHETAG", [key, "hkhalid"]); + var initEtag = db.Execute("SETWITHETAG", [key, "hkhalid"]); + ClassicAssert.AreEqual(0, long.Parse(initEtag.ToString())); RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", [key]); long etag = long.Parse(res[0].ToString()); @@ -140,7 +150,73 @@ public void GetIfNotMatchReturnsDataWhenEtagDoesNotMatch() # region Edgecases [Test] - public void SetWithEtagOnAlreadyExistingDataOverridesIt() + public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesItWithInitialEtag() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + long etag = (long)res; + ClassicAssert.AreEqual(0, etag); + + // update to value to update the etag + RedisResult[] updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); + etag = (long)updateRes[0]; + ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); + + // inplace update + res = db.Execute("SETWITHETAG", ["rizz", "meow"]); + etag = (long)res; + ClassicAssert.AreEqual(0, etag); + + // update to value to update the etag + updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); + etag = (long)updateRes[0]; + ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); + + // Copy update + res = db.Execute("SETWITHETAG", ["rizz", "oneofus"]); + etag = (long)res; + ClassicAssert.AreEqual(0, etag); + } + + [Test] + public void SetWithEtagWithRetainEtagOnAlreadyExistingSetWithEtagDataOverridesItButUpdatesEtag() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz", "RETAINETAG"]); + long etag = (long)res; + ClassicAssert.AreEqual(0, etag); + + // update to value to update the etag + RedisResult[] updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); + etag = (long)updateRes[0]; + ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); + + // inplace update + res = db.Execute("SETWITHETAG", ["rizz", "meow", "RETAINETAG"]); + etag = (long)res; + ClassicAssert.AreEqual(2, etag); + + // update to value to update the etag + updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); + etag = (long)updateRes[0]; + ClassicAssert.AreEqual(3, etag); + ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); + + // Copy update + res = db.Execute("SETWITHETAG", ["rizz", "oneofus", "RETAINETAG"]); + etag = (long)res; + ClassicAssert.AreEqual(4, etag); + } + + [Test] + public void SetWithEtagWithRetainEtagOnAlreadyExistingNonEtagDataOverridesItToInitialEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); @@ -148,7 +224,7 @@ public void SetWithEtagOnAlreadyExistingDataOverridesIt() ClassicAssert.IsTrue(db.StringSet("rizz", "used")); // inplace update - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz", "RETAINETAG"]); long etag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, etag); @@ -157,86 +233,99 @@ public void SetWithEtagOnAlreadyExistingDataOverridesIt() ClassicAssert.IsTrue(db.StringSet("rizz", "my")); // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "some"]); + res = db.Execute("SETWITHETAG", ["rizz", "some", "RETAINETAG"]); etag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, etag); } + #endregion + + #region ETAG Apis with non-etag data + [Test] - public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesIt() + public void SetWithEtagOnAlreadyExistingNonEtagDataOverridesIt() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); + + ClassicAssert.IsTrue(db.StringSet("rizz", "used")); + + // inplace update RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); long etag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, etag); - // inplace update - res = db.Execute("SETWITHETAG", ["rizz", "meow"]); - etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); + db.KeyDelete("rizz"); + + ClassicAssert.IsTrue(db.StringSet("rizz", "my")); // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "oneofus"]); + res = db.Execute("SETWITHETAG", ["rizz", "some"]); etag = long.Parse(res.ToString()); ClassicAssert.AreEqual(0, etag); } - #endregion - - #region ETAG Apis with non-etag data - - // ETAG Apis with non-Etag data just tests that in all scenarios we always return wrong data type response [Test] - public void SetIfMatchOnNonEtagDataReturnsWrongType() + public void SetWithEtagWithRetainEtagOnAlreadyExistingNonEtagDataOverridesIt() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); - var _ = db.StringSet("h", "k"); + ClassicAssert.IsTrue(db.StringSet("rizz", "used")); - RedisServerException ex = Assert.Throws(() => db.Execute("SETIFMATCH", ["h", "t", "0"])); + // inplace update + RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + long etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + db.KeyDelete("rizz"); - ex = Assert.Throws(() => db.Execute("SETIFMATCH", ["h", "t", "1"])); + ClassicAssert.IsTrue(db.StringSet("rizz", "my")); - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + // Copy update + res = db.Execute("SETWITHETAG", ["rizz", "some"]); + etag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(0, etag); } + [Test] - public void GetIfNotMatchOnNonEtagDataReturnsWrongType() + public void SetIfMatchOnNonEtagDataReturnsEtagMismatch() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); var _ = db.StringSet("h", "k"); - RedisServerException ex = Assert.Throws(() => db.Execute("GETIFNOTMATCH", ["h", "0"])); + var res = db.Execute("SETIFMATCH", ["h", "t", "0"]); + ClassicAssert.AreEqual("ETAGMISMATCH", res.ToString()); + } - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + [Test] + public void GetIfNotMatchOnNonEtagDataReturnsNilForEtagAndCorrectData() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); - ex = Assert.Throws(() => db.Execute("GETIFNOTMATCH", ["h", "1"])); + var _ = db.StringSet("h", "k"); - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + var res = (RedisResult[])db.Execute("GETIFNOTMATCH", ["h", "1"]); + + ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual("k", res[1].ToString()); } [Test] - public void GetWithEtagOnNonEtagDataReturnsWrongType() + public void GetWithEtagOnNonEtagDataReturnsNilForEtagAndCorrectData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); var _ = db.StringSet("h", "k"); - RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", ["h"])); - - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + var res = (RedisResult[])db.Execute("GETWITHETAG", ["h"]); + ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual("k", res[1].ToString()); } #endregion @@ -364,7 +453,7 @@ public void SetExpiryHighPrecisionForEtagSetDatat() } [Test] - public void SetGetForEtagSetData() + public void SetGetWithRetainEtagForEtagSetData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -379,8 +468,11 @@ public void SetGetForEtagSetData() // Smaller new value without expiration string newValue1 = "abcdefghijklmnopqrs"; - retValue = db.StringSetAndGet(key, newValue1, null, When.Always, CommandFlags.None); + + retValue = db.Execute("SET", [key, newValue1, "GET", "RETAINETAG"]).ToString(); + ClassicAssert.AreEqual(origValue, retValue); + retValue = db.StringGet(key); ClassicAssert.AreEqual(newValue1, retValue); @@ -390,7 +482,7 @@ public void SetGetForEtagSetData() // Smaller new value with KeepTtl string newValue2 = "abcdefghijklmnopqr"; - retValue = db.StringSetAndGet(key, newValue2, null, true, When.Always, CommandFlags.None); + retValue = db.Execute("SET", [key, newValue2, "GET", "RETAINETAG"]).ToString(); // This should increase the ETAG internally so we have a check for that here checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); @@ -404,7 +496,7 @@ public void SetGetForEtagSetData() // Smaller new value with expiration string newValue3 = "01234"; - retValue = db.StringSetAndGet(key, newValue3, TimeSpan.FromSeconds(10), When.Exists, CommandFlags.None); + retValue = db.Execute("SET", [key, newValue3, "EX", "10", "GET", "RETAINETAG"]).ToString(); ClassicAssert.AreEqual(newValue2, retValue); // This should increase the ETAG internally so we have a check for that here @@ -418,7 +510,7 @@ public void SetGetForEtagSetData() // Larger new value with expiration string newValue4 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - retValue = db.StringSetAndGet(key, newValue4, TimeSpan.FromSeconds(100), When.Exists, CommandFlags.None); + retValue = db.Execute("SET", [key, newValue4, "EX", "100", "GET", "RETAINETAG"]).ToString(); ClassicAssert.AreEqual(newValue3, retValue); // This should increase the ETAG internally so we have a check for that here @@ -432,7 +524,7 @@ public void SetGetForEtagSetData() // Smaller new value without expiration string newValue5 = "0123401234"; - retValue = db.StringSetAndGet(key, newValue5, null, When.Exists, CommandFlags.None); + retValue = db.Execute("SET", [key, newValue5, "GET", "RETAINETAG"]).ToString(); ClassicAssert.AreEqual(newValue4, retValue); retValue = db.StringGet(key); ClassicAssert.AreEqual(newValue5, retValue); @@ -445,7 +537,7 @@ public void SetGetForEtagSetData() // Larger new value without expiration string newValue6 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - retValue = db.StringSetAndGet(key, newValue6, null, When.Always, CommandFlags.None); + retValue = db.Execute("SET", [key, newValue6, "GET", "RETAINETAG"]).ToString(); ClassicAssert.AreEqual(newValue5, retValue); retValue = db.StringGet(key); ClassicAssert.AreEqual(newValue6, retValue); @@ -492,6 +584,8 @@ public void SetExpiryIncrForEtagSetData() ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(-99998, nRetVal); + var res69 = db.KeyTimeToLive(strKey); + Thread.Sleep(5000); // Expired key, restart increment,after exp this is treated as new record @@ -503,9 +597,9 @@ public void SetExpiryIncrForEtagSetData() ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(1, nRetVal); - RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [strKey])); - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + var etagGet = (RedisResult[])db.Execute("GETWITHETAG", [strKey]); + ClassicAssert.IsTrue(etagGet[0].IsNull); + ClassicAssert.AreEqual(1, Convert.ToInt64(etagGet[1])); } [Test] @@ -557,19 +651,52 @@ public void StringSetOnAnExistingEtagDataOverrides() long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); ClassicAssert.AreEqual(0, checkEtag); - // override the setwithetag to a new value altogether, this will make it lose it's etag capability - // This is a limitation for Etags because plain sets are upserts (blind updates), and currently we - // cannot increase the latency in the common path for set to check beyong Readonly address for the - // existence of a record with ETag. This means that sets are complete upserts and clients need to use - // setifmatch if they want each consequent set to maintain the key value pair's etag property + // Unless the SET was called with RETAINETAG a call to set will override the setwithetag to a new + // value altogether, this will make it lose it's etag capability. This is a limitation for Etags + // because plain sets are upserts (blind updates), and currently we cannot increase the latency in + // the common path for set to check beyong Readonly address for the existence of a record with ETag. + // This means that sets are complete upserts and clients need to use setifmatch, or set with RETAINETAG + // if they want each consequent set to maintain the key value pair's etag property. ClassicAssert.IsTrue(db.StringSet(strKey, "ciaociao")); string retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociao", retVal); + + var res = (RedisResult[])db.Execute("GETWITHETAG", [strKey]); + ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual("ciaociao", res[1].ToString()); + } - RedisServerException ex = Assert.Throws(() => db.Execute("GETWITHETAG", [strKey])); - ClassicAssert.IsNotNull(ex); - ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE), ex.Message); + [Test] + public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var strKey = "mykey"; + db.Execute("SETWITHETAG", [strKey, 9]); + + long checkEtag = (long)db.Execute("GETWITHETAG", [strKey])[0]; + ClassicAssert.AreEqual(0, checkEtag); + + // Unless you explicitly call SET with RETAINETAG option you will lose the etag on the previous key-value pair + db.Execute("SET", [strKey, "ciaociao", "RETAINETAG"]); + + string retVal = db.StringGet(strKey).ToString(); + ClassicAssert.AreEqual("ciaociao", retVal); + + var res = (RedisResult[]) db.Execute("GETWITHETAG", strKey); + ClassicAssert.AreEqual(1, (long)res[0]); + + // on subsequent upserts we are still increasing the etag transparently + db.Execute("SET", [strKey, "ciaociaociao", "RETAINETAG"]); + + retVal = db.StringGet(strKey).ToString(); + ClassicAssert.AreEqual("ciaociaociao", retVal); + + res = (RedisResult[]) db.Execute("GETWITHETAG", strKey); + ClassicAssert.AreEqual(2, (long)res[0]); + ClassicAssert.AreEqual("ciaociaociao", res[1].ToString()); } [Test] diff --git a/website/docs/commands/etag-commands.md b/website/docs/commands/etag-commands.md index 87e6a0fb75..4450853560 100644 --- a/website/docs/commands/etag-commands.md +++ b/website/docs/commands/etag-commands.md @@ -20,17 +20,20 @@ Compatibility with non-ETag commands and the behavior of data inserted with ETag #### **Syntax** ```bash -SETWITHETAG key value +SETWITHETAG key value [RETAINETAG] ``` Inserts a key-value string pair into Garnet, associating an ETag that will be updated upon changes to the value. +**Options:** + +* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If not etag existed for the previous key this will initialize one. + #### **Response** One of the following: - **Integer reply**: A response integer indicating the initial ETag value on success. -- **Error reply**: Returns an error if the key already exists. --- @@ -48,9 +51,8 @@ Retrieves the value and the ETag associated with the given key. One of the following: -- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. +- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. If called on a key-value pair without ETag, the first item will be nil. - **Nil reply**: If the key does not exist. -- **Error reply**: Returns an error if `GETWITHETAG` is called on a key that was not set with `SETWITHETAG`. --- @@ -70,8 +72,7 @@ One of the following: - **Integer reply**: The updated ETag if the value was successfully updated. - **Nil reply**: If the key does not exist. -- **Error reply (ETag mismatch)**: If the provided ETag does not match the current ETag. -- **Error reply**: Returns an error if `SETIFMATCH` is called on a key not set with `SETWITHETAG`. +- **Error reply (ETag mismatch)**: If the provided ETag does not match the current ETag. If the command is called on a record without an ETag we will return ETag mismatch as well. --- @@ -89,10 +90,9 @@ Retrieves the value if the ETag associated with the key has changed; otherwise, One of the following: -- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. +- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be nil. - **Nil reply**: If the key does not exist. - **Simple string reply**: Returns a string indicating the value is unchanged if the provided ETag matches the current ETag. -- **Error reply**: Returns an error if `GETIFNOTMATCH` is called on a key not set with `SETWITHETAG`. --- @@ -102,7 +102,8 @@ ETag commands executed on keys that were not set with `SETWITHETAG` will return Below is the expected behavior of ETag-associated key-value pairs when non-ETag commands are used. -- **SET, MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. +- **MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. +- **SET**: If only if used with additional option "RETAINETAG" will update the etag while inserting the key-value pair over the existing key-value pair. - **RENAME**: Renaming an ETag-associated key-value pair will reset the ETag to 0 for the renamed key. --- diff --git a/website/docs/commands/raw-string.md b/website/docs/commands/raw-string.md index 54e88e1dcf..46613fc730 100644 --- a/website/docs/commands/raw-string.md +++ b/website/docs/commands/raw-string.md @@ -207,7 +207,7 @@ Simple string reply: OK. #### Syntax ```bash - SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | + SET key value [NX | XX] [GET] [EX seconds | PX milliseconds] [KEEPTTL] [RETAINETAG] ``` Set **key** to hold the string value. If key already holds a value, it is overwritten, regardless of its type. Any previous time to live associated with the **key** is discarded on successful SET operation. @@ -219,6 +219,7 @@ Set **key** to hold the string value. If key already holds a value, it is overwr * NX -- Only set the key if it does not already exist. * XX -- Only set the key if it already exists. * KEEPTTL -- Retain the time to live associated with the key. +* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this initialize one. #### Resp Reply From 41bdb41aea852afe2c0838be754f0f3c4de9ed5d Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 1 Oct 2024 10:32:13 +1000 Subject: [PATCH 11/87] Update mainstore ops fixing rename bug and remove unnecessary GetForEtagCmd --- libs/common/RespReadUtils.cs | 37 +++++++++++ libs/server/API/GarnetApi.cs | 3 - libs/server/API/GarnetWatchApi.cs | 7 -- libs/server/API/IGarnetApi.cs | 5 -- libs/server/Resp/BasicCommands.cs | 5 +- .../Storage/Session/MainStore/MainStoreOps.cs | 66 ++++++++----------- 6 files changed, 66 insertions(+), 57 deletions(-) diff --git a/libs/common/RespReadUtils.cs b/libs/common/RespReadUtils.cs index 2ae39f723a..f537fa7633 100644 --- a/libs/common/RespReadUtils.cs +++ b/libs/common/RespReadUtils.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Text; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -377,6 +378,42 @@ public static bool TryRead64Int(out long number, ref byte* ptr, byte* end, out b return true; } + /// + /// Given a buffer check if the value is nil ($-1\r\n) + /// If the value is nil it advances the buffer forward + /// + /// The starting position in the RESP string. Will be advanced if parsing is successful. + /// The current end of the RESP string. + /// + /// True if value is nil on the buffer, false if the value on buffer is not nil + public static bool ReadNil(ref byte* ptr, byte* end, out byte? unexpectedToken) + { + unexpectedToken = null; + if (end - ptr < 5) + { + return false; + } + + ReadOnlySpan ptrNext5Bytes = new ReadOnlySpan(ptr, 5); + ReadOnlySpan expectedNilRepr = "$-1\r\n"u8; + if (!ptrNext5Bytes.SequenceEqual(expectedNilRepr)) + { + for (int i = 0; i < 5; i++) + { + if (expectedNilRepr[i] != ptrNext5Bytes[i]) + { + unexpectedToken = ptrNext5Bytes[i]; + return false; + } + } + // If the sequence is not equal we shouldn't even reach this because atleast one byte should have mismatched + Debug.Assert(false); + return false; + } + ptr += 5; + return true; + } + /// /// Tries to read a RESP array length header from the given ASCII-encoded RESP string /// and, if successful, moves the given ptr to the end of the length header. diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index 66dd0fba77..6896dbd94d 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -51,9 +51,6 @@ public void WATCH(byte[] key, StoreType type) public GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) => storageSession.GET(ref key, ref input, ref output, ref context); - public GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) - => storageSession.GETForETagCmd(ref key, ref input, ref output, ref context); - /// public GarnetStatus GET_WithPending(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, long ctx, out bool pending) => storageSession.GET_WithPending(ref key, ref input, ref output, ctx, out pending, ref context); diff --git a/libs/server/API/GarnetWatchApi.cs b/libs/server/API/GarnetWatchApi.cs index d93bc9ff97..bbae63343a 100644 --- a/libs/server/API/GarnetWatchApi.cs +++ b/libs/server/API/GarnetWatchApi.cs @@ -29,13 +29,6 @@ public GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref SpanByteAndMem return garnetApi.GET(ref key, ref input, ref output); } - public GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output) - { - garnetApi.WATCH(new ArgSlice(ref key), StoreType.Main); - return garnetApi.GETForETagCmd(ref key, ref input, ref output); - } - - /// public GarnetStatus GETForMemoryResult(ArgSlice key, out MemoryResult value) { diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index d8c9b30a6d..80e8a27a48 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -1000,11 +1000,6 @@ public interface IGarnetReadApi /// GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output); - /// - /// GET - /// - GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output); - /// /// GET /// diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index dea767ddda..18ada43c79 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -288,7 +288,7 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; ((RespInputHeader*)inputPtr)->flags = 0; - var status = storageApi.GETForETagCmd(ref key, ref input, ref output); + var status = storageApi.GET(ref key, ref input, ref output); switch (status) { @@ -336,7 +336,7 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) ((RespInputHeader*)inputPtr)->flags = 0; *(long*)(inputPtr + RespInputHeader.Size) = etagToCheckWith; - var status = storageApi.GETForETagCmd(ref key, ref input, ref output); + var status = storageApi.GET(ref key, ref input, ref output); switch (status) { @@ -708,7 +708,6 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) } else if (nextOpt.SequenceEqual(CmdStrings.GET)) { - // tokenIdx++; // HK TODO: WHY TAL WHYYYY!????? getValue = true; } else if (nextOpt.SequenceEqual(CmdStrings.RETAINETAG)) diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 76179ce4c4..95031a77ac 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using Garnet.common; @@ -42,40 +43,6 @@ public GarnetStatus GET(ref SpanByte key, ref SpanByte input, ref Span } } - // We separate this function altogether for being able to filter out WONGTYPE so no overhead is incurred in the common path from branching - // that this method call would have added in the instructions. This let's GET method calls function without overhead as they did before ETags - // were added - public GarnetStatus GETForETagCmd(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, ref TContext context) - where TContext : ITsavoriteContext - { - long ctx = default; - var status = context.Read(ref key, ref input, ref output, ctx); - - if (status.IsPending) - { - StartPendingMetrics(); - CompletePendingForSession(ref status, ref output, ref context); - StopPendingMetrics(); - } - - if (status.IsCanceled) - { - // Cancelled Read operation on a Get call for any ETag based APIs inidcates that the cmd was applied to a non-etag record - return GarnetStatus.WRONGTYPE; - } - - if (status.Found) - { - incr_session_found(); - return GarnetStatus.OK; - } - else - { - incr_session_notfound(); - return GarnetStatus.NOTFOUND; - } - } - public unsafe GarnetStatus ReadWithUnsafeContext(ArgSlice key, ref SpanByte input, ref SpanByteAndMemory output, long localHeadAddress, out bool epochChanged, ref TContext context) where TContext : ITsavoriteContext, IUnsafeContext { @@ -636,6 +603,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S if (status == GarnetStatus.OK) { + // since we didn't give the output span any memory when creating it, the backend would necessarily have had to allocate heap memory if item is not NOTFOUND Debug.Assert(!o.IsSpanByte); var memoryHandle = o.Memory.Memory.Pin(); var ptrVal = (byte*)memoryHandle.Pointer; @@ -652,8 +620,20 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; ((RespInputHeader*)inputPtr)->flags = 0; - var etagAndDataOutput = new SpanByteAndMemory(); - var getWithEtagStatus = GETForETagCmd(ref oldKey, ref getWithEtagInput, ref etagAndDataOutput, ref context); + // Check if etag is nil for getWithEtagStatus, this tells us if the key already had an etag associated with it + bool hasEtag = false; + SpanByteAndMemory etagAndDataOutput = new SpanByteAndMemory(); + GarnetStatus getWithEtagStatus = GET(ref oldKey, ref getWithEtagInput, ref etagAndDataOutput, ref context); + // we know this key exists, so there is no reason for the status to not be OK + Debug.Assert(getWithEtagStatus == GarnetStatus.OK); + // since we didn't give the etagAndDataOutput span any memory when creating it, the backend would necessarily have had to allocate heap memory if item is not NOTFOUND + Debug.Assert(!etagAndDataOutput.IsSpanByte); + MemoryHandle outputMemHandle = etagAndDataOutput.Memory.Memory.Pin(); + byte* outputBufCurr = (byte*)outputMemHandle.Pointer; + byte* end = outputBufCurr + etagAndDataOutput.Length; + RespReadUtils.ReadUnsignedArrayLength(out int numItemInArr, ref outputBufCurr, end); + Debug.Assert(numItemInArr == 2); + hasEtag = !RespReadUtils.ReadNil(ref outputBufCurr, end, out byte? _); if (ttlStatus == GarnetStatus.OK && !expireSpan.IsSpanByte) { @@ -662,7 +642,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _); // If the key has an expiration, set the new key with the expiration - if (expireTimeMs > 0 && getWithEtagStatus == GarnetStatus.WRONGTYPE) + if (expireTimeMs > 0 && !hasEtag) { if (isNX) { @@ -685,7 +665,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context); } } - else if (expireTimeMs == -1 && getWithEtagStatus == GarnetStatus.WRONGTYPE) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key + else if (expireTimeMs == -1 && !hasEtag) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key { if (isNX) { @@ -711,7 +691,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S } else if ( (expireTimeMs == -1 || expireTimeMs > 0) && - getWithEtagStatus == GarnetStatus.OK) + hasEtag) { SpanByte newKey = newKeySlice.SpanByte; @@ -734,6 +714,10 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S *(int*)valPtr = RespInputHeader.Size + value.Length; ((RespInputHeader*)(valPtr + sizeof(int)))->cmd = RespCommand.SETWITHETAG; ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; + ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; + // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag + // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key + ((RespInputHeader*)(valPtr + sizeof(int)))->SetRetainEtagFlag(); } else { @@ -743,6 +727,9 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S *(int*)valPtr = sizeof(long) + RespInputHeader.Size + value.Length; ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->cmd = RespCommand.SETWITHETAG; ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->flags = 0; + // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag + // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key + ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->SetRetainEtagFlag(); SpanByte.Reinterpret(inputPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; } @@ -751,6 +738,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S ref Unsafe.AsRef(valPtr), ref context); } + outputMemHandle.Dispose(); expireSpan.Memory.Dispose(); memoryHandle.Dispose(); o.Memory.Dispose(); From f6e2fb327588b9ccb4a917b1416f9ccefdb1877c Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 1 Oct 2024 10:54:23 +1000 Subject: [PATCH 12/87] add read nil test --- libs/common/RespReadUtils.cs | 4 ++++ test/Garnet.test/Resp/RespReadUtilsTests.cs | 24 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/libs/common/RespReadUtils.cs b/libs/common/RespReadUtils.cs index f537fa7633..cbb388bf34 100644 --- a/libs/common/RespReadUtils.cs +++ b/libs/common/RespReadUtils.cs @@ -400,8 +400,11 @@ public static bool ReadNil(ref byte* ptr, byte* end, out byte? unexpectedToken) { for (int i = 0; i < 5; i++) { + // first place where the sequence differs we have found the unexpected token if (expectedNilRepr[i] != ptrNext5Bytes[i]) { + // move the pointer to the unexpected token + ptr += i; unexpectedToken = ptrNext5Bytes[i]; return false; } @@ -410,6 +413,7 @@ public static bool ReadNil(ref byte* ptr, byte* end, out byte? unexpectedToken) Debug.Assert(false); return false; } + ptr += 5; return true; } diff --git a/test/Garnet.test/Resp/RespReadUtilsTests.cs b/test/Garnet.test/Resp/RespReadUtilsTests.cs index 7860dd7bc2..b07240f3f8 100644 --- a/test/Garnet.test/Resp/RespReadUtilsTests.cs +++ b/test/Garnet.test/Resp/RespReadUtilsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System; using System.Text; using Garnet.common; using Garnet.common.Parsing; @@ -288,5 +289,28 @@ public static unsafe void ReadBoolWithLengthHeaderTest(string text, bool expecte ClassicAssert.IsTrue(start == end); } } + + /// + /// Tests that Readnil successfully parses valid inputs. + /// + [TestCase("", false, null)] // Too short + [TestCase("S$-1\r\n", false, "S")] // Long enough but not nil leading + [TestCase("$-1\n1738\r\n", false, "1")] // Long enough but not nil + [TestCase("$-1\r\n", true, null)] // exact nil + [TestCase("$-1\r\nxyzextra", true, null)] // leading nil but with extra bytes after + public static unsafe void ReadBoolWithLengthHeaderTest(string testSequence, bool expected, string firstMismatch) + { + ReadOnlySpan testSeq = new ReadOnlySpan(Encoding.ASCII.GetBytes(testSequence)); + + fixed (byte* ptr = testSeq) + { + byte* start = ptr; + byte* end = ptr + testSeq.Length; + var isNil = RespReadUtils.ReadNil(ref start, end, out byte? unexpectedToken); + + ClassicAssert.AreEqual(expected, isNil); + ClassicAssert.AreEqual((byte?)firstMismatch?[0], unexpectedToken); + } + } } } \ No newline at end of file From e89477d93ffa746e06926c4428b9f360d3ebfd17 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 1 Oct 2024 11:09:54 +1000 Subject: [PATCH 13/87] fmt --- libs/server/Resp/BasicCommands.cs | 13 +------- .../Functions/MainStore/PrivateMethods.cs | 4 +-- .../Storage/Functions/MainStore/RMWMethods.cs | 2 +- .../Functions/MainStore/ReadMethods.cs | 32 +++++++++---------- test/Garnet.test/RespEtagTests.cs | 14 ++++---- 5 files changed, 27 insertions(+), 38 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 18ada43c79..ae78ea753e 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -297,10 +297,6 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); break; - case GarnetStatus.WRONGTYPE: - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) - SendAndReset(); - break; default: if (!output.IsSpanByte) SendAndReset(output.Memory, output.Length); @@ -345,10 +341,6 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); break; - case GarnetStatus.WRONGTYPE: - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) - SendAndReset(); - break; default: if (!output.IsSpanByte) SendAndReset(output.Memory, output.Length); @@ -919,14 +911,11 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); break; + // SETIFNOTMATCH is the only one who can return this and is always called with getvalue so we only handle this here case GarnetStatus.ETAGMISMATCH: while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) SendAndReset(); break; - case GarnetStatus.WRONGTYPE: - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) - SendAndReset(); - break; default: if (!o.IsSpanByte) SendAndReset(o.Memory, o.Length); diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 5fcffd024a..8fed039ce9 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -235,7 +235,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd // we know somethgin doesnt have an etag if etag = -1; valueLength = value.LengthWithoutMetadata; - + if (hasEtagInVal) { // Get value without RESP header; exclude expiration @@ -250,7 +250,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd } else { - etagTruncatedVal = value.AsReadOnlySpan(); + etagTruncatedVal = value.AsReadOnlySpan(); // instead of :(etagdigits) we will have nil after array len desiredLength = 4 + 3 + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 7717e5e02b..9e30a4713e 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -735,7 +735,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa nextUpdateEtagIgnoredEnd = -1; recordInfo.ClearHasETag(); } - + // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length - etagIgnoredOffset); diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 90a7a96b73..945b258079 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -24,14 +24,14 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { - long existingEtag = *(long*)value.ToPointer(); - long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); - if (existingEtag == etagToMatchAgainst) - { - // write the value not changed message to dst, and early return - CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); - return true; - } + long existingEtag = *(long*)value.ToPointer(); + long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag == etagToMatchAgainst) + { + // write the value not changed message to dst, and early return + CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + return true; + } } else if ((byte)cmd >= CustomCommandManager.StartOffset) { @@ -77,14 +77,14 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { - long existingEtag = *(long*)value.ToPointer(); - long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); - if (existingEtag == etagToMatchAgainst) - { - // write the value not changed message to dst, and early return - CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); - return true; - } + long existingEtag = *(long*)value.ToPointer(); + long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + if (existingEtag == etagToMatchAgainst) + { + // write the value not changed message to dst, and early return + CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + return true; + } } else if ((byte)cmd >= CustomCommandManager.StartOffset) { diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 270a74f72a..20d80dc30a 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -160,7 +160,7 @@ public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesItWithInitialEta ClassicAssert.AreEqual(0, etag); // update to value to update the etag - RedisResult[] updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); + RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(1, etag); ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); @@ -171,7 +171,7 @@ public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesItWithInitialEta ClassicAssert.AreEqual(0, etag); // update to value to update the etag - updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); + updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(1, etag); ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); @@ -193,7 +193,7 @@ public void SetWithEtagWithRetainEtagOnAlreadyExistingSetWithEtagDataOverridesIt ClassicAssert.AreEqual(0, etag); // update to value to update the etag - RedisResult[] updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); + RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(1, etag); ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); @@ -204,7 +204,7 @@ public void SetWithEtagWithRetainEtagOnAlreadyExistingSetWithEtagDataOverridesIt ClassicAssert.AreEqual(2, etag); // update to value to update the etag - updateRes = (RedisResult[]) db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); + updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(3, etag); ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); @@ -661,7 +661,7 @@ public void StringSetOnAnExistingEtagDataOverrides() string retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociao", retVal); - + var res = (RedisResult[])db.Execute("GETWITHETAG", [strKey]); ClassicAssert.IsTrue(res[0].IsNull); ClassicAssert.AreEqual("ciaociao", res[1].ToString()); @@ -685,7 +685,7 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() string retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociao", retVal); - var res = (RedisResult[]) db.Execute("GETWITHETAG", strKey); + var res = (RedisResult[])db.Execute("GETWITHETAG", strKey); ClassicAssert.AreEqual(1, (long)res[0]); // on subsequent upserts we are still increasing the etag transparently @@ -694,7 +694,7 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociaociao", retVal); - res = (RedisResult[]) db.Execute("GETWITHETAG", strKey); + res = (RedisResult[])db.Execute("GETWITHETAG", strKey); ClassicAssert.AreEqual(2, (long)res[0]); ClassicAssert.AreEqual("ciaociaociao", res[1].ToString()); } From ae5dd439fabd06612ffcb14c7df8817910bb0834 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 1 Oct 2024 12:31:39 +1000 Subject: [PATCH 14/87] Update docs and add more tests for edgecase --- .../Functions/MainStore/PrivateMethods.cs | 41 ++++++++-------- .../Functions/MainStore/VarLenInputMethods.cs | 2 +- test/Garnet.test/Resp/RespReadUtilsTests.cs | 4 +- test/Garnet.test/RespEtagTests.cs | 31 ++++++++++++ website/docs/commands/etag-commands.md | 49 +++---------------- website/docs/commands/raw-string.md | 2 +- 6 files changed, 62 insertions(+), 67 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 8fed039ce9..d8a6b2bace 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -36,7 +36,8 @@ static void CopyTo(ref SpanByte src, ref SpanByteAndMemory dst, MemoryPool void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int end = -1) { - int srcLength = end == -1 ? src.LengthWithoutMetadata : Math.Max(end - start, 0); + // src length of the value indicating no end is supplied defaults to lengthWithoutMetadata, else it chooses the bigger of 0 or (end - start) + int srcLength = end == -1 ? src.LengthWithoutMetadata : ((start < end) ? (end - start) : 0); if (srcLength == 0) { CopyDefaultResp(CmdStrings.RESP_EMPTY, ref dst); @@ -82,7 +83,7 @@ void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int } } - void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending, int payloadEtagEnd, int etagIgnoredDataEnd, bool hasEtagInVal) + void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending, int payloadEtagAccountedEndOffset, int etagAccountedEnd, bool hasEtagInVal) { var inputPtr = input.ToPointer(); switch ((RespCommand)(*inputPtr)) @@ -93,7 +94,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd // This is accomplished by calling ConvertToHeap on the destination SpanByteAndMemory if (isFromPending) dst.ConvertToHeap(); - CopyRespTo(ref value, ref dst, payloadEtagEnd, etagIgnoredDataEnd); + CopyRespTo(ref value, ref dst, payloadEtagAccountedEndOffset, etagAccountedEnd); break; case RespCommand.MIGRATE: @@ -123,19 +124,19 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd // Get value without RESP header; exclude expiration if (value.LengthWithoutMetadata <= dst.Length) { - dst.Length = value.LengthWithoutMetadata - payloadEtagEnd; - value.AsReadOnlySpan(payloadEtagEnd).CopyTo(dst.SpanByte.AsSpan()); + dst.Length = value.LengthWithoutMetadata - payloadEtagAccountedEndOffset; + value.AsReadOnlySpan(payloadEtagAccountedEndOffset).CopyTo(dst.SpanByte.AsSpan()); return; } dst.ConvertToHeap(); - dst.Length = value.LengthWithoutMetadata - payloadEtagEnd; + dst.Length = value.LengthWithoutMetadata - payloadEtagAccountedEndOffset; dst.Memory = functionsState.memoryPool.Rent(value.LengthWithoutMetadata); - value.AsReadOnlySpan(payloadEtagEnd).CopyTo(dst.Memory.Memory.Span); + value.AsReadOnlySpan(payloadEtagAccountedEndOffset).CopyTo(dst.Memory.Memory.Span); break; case RespCommand.GETBIT: - byte oldValSet = BitmapManager.GetBit(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); + byte oldValSet = BitmapManager.GetBit(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); if (oldValSet == 0) CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_0, ref dst); else @@ -143,18 +144,18 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd break; case RespCommand.BITCOUNT: - long count = BitmapManager.BitCountDriver(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); + long count = BitmapManager.BitCountDriver(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); CopyRespNumber(count, ref dst); break; case RespCommand.BITPOS: - long pos = BitmapManager.BitPosDriver(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); + long pos = BitmapManager.BitPosDriver(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); *(long*)dst.SpanByte.ToPointer() = pos; CopyRespNumber(pos, ref dst); break; case RespCommand.BITOP: - IntPtr bitmap = (IntPtr)value.ToPointer() + payloadEtagEnd; + IntPtr bitmap = (IntPtr)value.ToPointer() + payloadEtagAccountedEndOffset; byte* output = dst.SpanByte.ToPointer(); *(long*)output = (long)bitmap.ToInt64(); @@ -165,7 +166,7 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd case RespCommand.BITFIELD: long retValue = 0; bool overflow; - (retValue, overflow) = BitmapManager.BitFieldExecute(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd); + (retValue, overflow) = BitmapManager.BitFieldExecute(inputPtr + RespInputHeader.Size, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); if (!overflow) CopyRespNumber(retValue, ref dst); else @@ -173,28 +174,28 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd return; case RespCommand.PFCOUNT: - if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd)) + if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset)) { *(long*)dst.SpanByte.ToPointer() = -1; return; } long E = 13; - E = HyperLogLog.DefaultHLL.Count(value.ToPointer() + payloadEtagEnd); + E = HyperLogLog.DefaultHLL.Count(value.ToPointer() + payloadEtagAccountedEndOffset); *(long*)dst.SpanByte.ToPointer() = E; return; case RespCommand.PFMERGE: - if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagEnd, value.Length - payloadEtagEnd)) + if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset)) { *(long*)dst.SpanByte.ToPointer() = -1; return; } - if (value.Length - payloadEtagEnd <= dst.Length) + if (value.Length - payloadEtagAccountedEndOffset <= dst.Length) { - Buffer.MemoryCopy(value.ToPointer() + payloadEtagEnd, dst.SpanByte.ToPointer(), value.Length - payloadEtagEnd, value.Length - payloadEtagEnd); - dst.SpanByte.Length = value.Length - payloadEtagEnd; + Buffer.MemoryCopy(value.ToPointer() + payloadEtagAccountedEndOffset, dst.SpanByte.ToPointer(), value.Length - payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); + dst.SpanByte.Length = value.Length - payloadEtagAccountedEndOffset; return; } throw new GarnetException("Not enough space in PFMERGE buffer"); @@ -210,12 +211,12 @@ void CopyRespToWithInput(ref SpanByte input, ref SpanByte value, ref SpanByteAnd return; case RespCommand.GETRANGE: - int len = value.LengthWithoutMetadata - payloadEtagEnd; + int len = value.LengthWithoutMetadata - payloadEtagAccountedEndOffset; int start = *(int*)(inputPtr + RespInputHeader.Size); int end = *(int*)(inputPtr + RespInputHeader.Size + 4); (start, end) = NormalizeRange(start, end, len); - CopyRespTo(ref value, ref dst, start + payloadEtagEnd, end + payloadEtagEnd); + CopyRespTo(ref value, ref dst, start + payloadEtagAccountedEndOffset, end + payloadEtagAccountedEndOffset); return; case RespCommand.SETIFMATCH: // extract ETAG, write as long into dst, and then value diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 29c1377b2e..1b3836bfb5 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -112,7 +112,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool ha var inputspan = input.AsSpan(); var inputPtr = input.ToPointer(); var cmd = inputspan[0]; - int etagOffset = hasEtag ? 8 : 0; + int etagOffset = hasEtag ? sizeof(long) : 0; bool retainEtag = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag(); switch ((RespCommand)cmd) diff --git a/test/Garnet.test/Resp/RespReadUtilsTests.cs b/test/Garnet.test/Resp/RespReadUtilsTests.cs index b07240f3f8..c0a576d7c3 100644 --- a/test/Garnet.test/Resp/RespReadUtilsTests.cs +++ b/test/Garnet.test/Resp/RespReadUtilsTests.cs @@ -295,10 +295,10 @@ public static unsafe void ReadBoolWithLengthHeaderTest(string text, bool expecte /// [TestCase("", false, null)] // Too short [TestCase("S$-1\r\n", false, "S")] // Long enough but not nil leading - [TestCase("$-1\n1738\r\n", false, "1")] // Long enough but not nil + [TestCase("$-1\n1738\r\n", false, "\n")] // Long enough but not nil [TestCase("$-1\r\n", true, null)] // exact nil [TestCase("$-1\r\nxyzextra", true, null)] // leading nil but with extra bytes after - public static unsafe void ReadBoolWithLengthHeaderTest(string testSequence, bool expected, string firstMismatch) + public static unsafe void ReadNilTest(string testSequence, bool expected, string firstMismatch) { ReadOnlySpan testSeq = new ReadOnlySpan(Encoding.ASCII.GetBytes(testSequence)); diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 20d80dc30a..a484877d78 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1911,6 +1911,37 @@ public void HyperLogLogCommandsShouldReturnWrongTypeErrorForEtagSetData() ClassicAssert.AreEqual(Encoding.ASCII.GetString(CmdStrings.RESP_ERR_WRONG_TYPE_HLL), ex.Message); } + [Test] + public void SetWithRetainEtagOnANewUpsertWillCreateKeyValueWithoutEtag() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string key = "mickey"; + string val = "mouse"; + + // a new upsert on a non-existing key will retain the "nil" etag + db.Execute("SET", [key, val, "RETAINETAG"]).ToString(); + + RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + RedisResult etag = res[0]; + string value = res[1].ToString(); + + ClassicAssert.IsTrue(etag.IsNull); + ClassicAssert.AreEqual(val, value); + + string newval = "clubhouse"; + + // a new upsert on an existing key will retain the "nil" etag from the prev + db.Execute("SET", [key, newval, "RETAINETAG"]).ToString(); + res = (RedisResult[])db.Execute("GETWITHETAG", [key]); + etag = res[0]; + value = res[1].ToString(); + + ClassicAssert.IsTrue(etag.IsNull); + ClassicAssert.AreEqual(newval, value); + } + #endregion } } \ No newline at end of file diff --git a/website/docs/commands/etag-commands.md b/website/docs/commands/etag-commands.md index 4450853560..7d1cef53be 100644 --- a/website/docs/commands/etag-commands.md +++ b/website/docs/commands/etag-commands.md @@ -31,8 +31,6 @@ Inserts a key-value string pair into Garnet, associating an ETag that will be up #### **Response** -One of the following: - - **Integer reply**: A response integer indicating the initial ETag value on success. --- @@ -72,7 +70,7 @@ One of the following: - **Integer reply**: The updated ETag if the value was successfully updated. - **Nil reply**: If the key does not exist. -- **Error reply (ETag mismatch)**: If the provided ETag does not match the current ETag. If the command is called on a record without an ETag we will return ETag mismatch as well. +- **Simple string reply**: If the provided ETag does not match the current ETag or If the command is called on a record without an ETag a simple string indicating ETag mismatch is returned. --- @@ -92,55 +90,20 @@ One of the following: - **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be nil. - **Nil reply**: If the key does not exist. -- **Simple string reply**: Returns a string indicating the value is unchanged if the provided ETag matches the current ETag. +- **Simple string reply**: if the provided ETag matches the current ETag, returns a simple string indicating the value is unchanged. --- ## Compatibility and Behavior with Non-ETag Commands -ETag commands executed on keys that were not set with `SETWITHETAG` will return a type mismatch error. Additionally, invoking `SETWITHETAG` on an existing key will overwrite the key-value pair and reset the associated ETag. - Below is the expected behavior of ETag-associated key-value pairs when non-ETag commands are used. - **MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. -- **SET**: If only if used with additional option "RETAINETAG" will update the etag while inserting the key-value pair over the existing key-value pair. -- **RENAME**: Renaming an ETag-associated key-value pair will reset the ETag to 0 for the renamed key. ---- +- **SET**: Only if used with additional option "RETAINETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. + +- **RENAME**: Renaming an ETag-associated key-value pair will reset the ETag to 0 for the renamed key. Unless the key being renamed to already existed before hand, in that case it will retain the etag of the existing key that was the target of the rename. -### **Same Behavior as Non-ETag Key-Value Pairs** - -The following commands do not expose the ETag to the user and behave the same as non-ETag key-value pairs. From the user's perspective, there is no indication that a key-value pair is associated with an ETag. - -- **GET** -- **DEL** -- **EXISTS** -- **EXPIRE** -- **PEXPIRE** -- **PERSIST** -- **GETRANGE** -- **TTL** -- **PTTL** -- **GETDEL** -- **STRLEN** -- **GETBIT** -- **BITCOUNT** -- **BITPOS** -- **BITFIELD_RO** - -### **Commands That Update ETag Internally** - -The following commands update the underlying data and consequently update the ETag of the key-value pair. However, the new ETag will not be exposed to the user until explicitly retrieved via an ETag-related command. - -- **SETRANGE** -- **APPEND** -- **INCR** -- **INCRBY** -- **DECR** -- **DECRBY** -- **SETBIT** -- **UNLINK** -- **MGET** -- **BITFIELD** +All other commands will update the etag internally if they modify the underlying data, and any responses from them will not expose the etag to the client. To the users the etag and it's updates remain hidden in non-etag commands. --- \ No newline at end of file diff --git a/website/docs/commands/raw-string.md b/website/docs/commands/raw-string.md index 46613fc730..a6c46c5763 100644 --- a/website/docs/commands/raw-string.md +++ b/website/docs/commands/raw-string.md @@ -219,7 +219,7 @@ Set **key** to hold the string value. If key already holds a value, it is overwr * NX -- Only set the key if it does not already exist. * XX -- Only set the key if it already exists. * KEEPTTL -- Retain the time to live associated with the key. -* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this initialize one. +* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this will create the new key-value pair without any etag as well. #### Resp Reply From 98e4ec3c6e9b74979f4d9864512e644d90dc70cf Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 1 Oct 2024 15:20:11 +1000 Subject: [PATCH 15/87] fix renamenx and etag compat --- .../Storage/Session/MainStore/MainStoreOps.cs | 17 +++++- test/Garnet.test/RespEtagTests.cs | 59 +++++++++++++++++++ test/Garnet.test/RespTests.cs | 42 +++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 95031a77ac..f4bd4e72c4 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -693,6 +693,19 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S (expireTimeMs == -1 || expireTimeMs > 0) && hasEtag) { + // IsNX means if the newKey Exists do not set it + // We can't use SET with not exist here so instead we will do an Exists and skip the seeting if exists + if (isNX && EXISTS(newKeySlice, storeType, ref context, ref objectContext) == GarnetStatus.OK) + { + result = 0; + returnStatus = GarnetStatus.OK; + // Skip setting the new key and go to calling the part after that + goto AFTERNEWKEYSET; + } + + if (isNX) + result = 1; + SpanByte newKey = newKeySlice.SpanByte; var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); @@ -714,7 +727,6 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S *(int*)valPtr = RespInputHeader.Size + value.Length; ((RespInputHeader*)(valPtr + sizeof(int)))->cmd = RespCommand.SETWITHETAG; ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; - ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key ((RespInputHeader*)(valPtr + sizeof(int)))->SetRetainEtagFlag(); @@ -736,8 +748,11 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S SET_Conditional(ref Unsafe.AsRef(keyPtr), ref Unsafe.AsRef(valPtr), ref context); + returnStatus = GarnetStatus.OK; } + AFTERNEWKEYSET: + outputMemHandle.Dispose(); expireSpan.Memory.Dispose(); memoryHandle.Dispose(); diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index a484877d78..d59c541282 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1098,6 +1098,36 @@ public void SingleRenameEtagSetData() ClassicAssert.AreEqual(null, origValue); } + [Test] + public void SingleRenameEtagShouldRetainEtagOfNewKeyIfExistsWithEtag() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string existingNewKey = "key2"; + string existingVal = "foo"; + long etag = (long)db.Execute("SETWITHETAG", [existingNewKey, existingVal]); + ClassicAssert.AreEqual(0, etag); + RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", [existingNewKey, "updated", etag.ToString()]); + long updatedEtag = (long)updateRes[0]; + + string origValue = "test1"; + etag = long.Parse(db.Execute("SETWITHETAG", ["key1", origValue]).ToString()); + ClassicAssert.AreEqual(0, etag); + + db.KeyRename("key1", existingNewKey); + string retValue = db.StringGet(existingNewKey); + ClassicAssert.AreEqual(origValue, retValue); + + // new Key value pair created with older value, the etag is reusing the existingnewkey etag + var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); + ClassicAssert.AreEqual(updatedEtag + 1, (long)res[0]); + ClassicAssert.AreEqual(origValue, res[1].ToString()); + + origValue = db.StringGet("key1"); + ClassicAssert.AreEqual(null, origValue); + } + [Test] public void SingleRenameKeyEdgeCaseEtagSetData([Values] bool withoutObjectStore) { @@ -1131,6 +1161,35 @@ public void SingleRenameKeyEdgeCaseEtagSetData([Values] bool withoutObjectStore) ClassicAssert.AreEqual(origValue, retValue); } + [Test] + public void SingleRenameShouldNotAddEtagEvenIfExistingKeyHadEtagButNotTheOriginal() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string existingNewKey = "key2"; + string existingVal = "foo"; + long etag = (long)db.Execute("SETWITHETAG", [existingNewKey, existingVal]); + ClassicAssert.AreEqual(0, etag); + RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", [existingNewKey, "updated", etag.ToString()]); + long updatedEtag = (long)updateRes[0]; + + string origValue = "test1"; + ClassicAssert.IsTrue(db.StringSet("key1", origValue)); + + db.KeyRename("key1", existingNewKey); + string retValue = db.StringGet(existingNewKey); + ClassicAssert.AreEqual(origValue, retValue); + + // new Key value pair created with older value, the etag is reusing the existingnewkey etag + var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); + ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual(origValue, res[1].ToString()); + + origValue = db.StringGet("key1"); + ClassicAssert.AreEqual(null, origValue); + } + [Test] public void PersistTTLTestForEtagSetData() { diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index ac5a60fff8..91f77e8a36 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -1506,6 +1506,48 @@ public void SingleRenameNxWithOldKeyAndNewKeyAsSame() ClassicAssert.AreEqual(origValue, retValue); } + [Test] + public void SingleRenameNXWithEtagSetOldAndNewKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var origValue = "test1"; + var key = "key1"; + var newKey = "key2"; + + db.Execute("SETWITHETAG", key, origValue); + db.Execute("SETWITHETAG", newKey, "foo"); + + var result = db.KeyRename(key, newKey, When.NotExists); + ClassicAssert.IsFalse(result); + } + + [Test] + public void SingleRenameNXWithEtagSetOldKey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var origValue = "test1"; + var key = "key1"; + var newKey = "key2"; + + db.Execute("SETWITHETAG", key, origValue); + + var result = db.KeyRename(key, newKey, When.NotExists); + ClassicAssert.IsTrue(result); + + string retValue = db.StringGet(newKey); + ClassicAssert.AreEqual(origValue, retValue); + + var oldKeyRes = db.StringGet(key); + ClassicAssert.IsTrue(oldKeyRes.IsNull); + + // Since the original key was set with etag, the new key should have an etag attached to it + var etagRes = (RedisResult[])db.Execute("GETWITHETAG", newKey); + ClassicAssert.AreEqual(0, (long)etagRes[0]); + ClassicAssert.AreEqual(origValue, etagRes[1].ToString()); + } + #endregion [Test] From da484170afe365cc8b08e9411526b2147a9daf9c Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 1 Oct 2024 15:28:43 +1000 Subject: [PATCH 16/87] Add more etag edge case tests --- test/Garnet.test/RespEtagTests.cs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index d59c541282..2ff249dc4a 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1190,6 +1190,35 @@ public void SingleRenameShouldNotAddEtagEvenIfExistingKeyHadEtagButNotTheOrigina ClassicAssert.AreEqual(null, origValue); } + [Test] + public void SingleRenameShouldAddEtagIfOldKeyHadEtagButNotExistingNewkey() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string existingNewKey = "key2"; + string existingVal = "foo"; + + ClassicAssert.IsTrue(db.StringSet(existingNewKey, existingVal)); + + string origValue = "test1"; + long etag = (long)db.Execute("SETWITHETAG", ["key1", origValue]); + ClassicAssert.AreEqual(0, etag); + + db.KeyRename("key1", existingNewKey); + string retValue = db.StringGet(existingNewKey); + ClassicAssert.AreEqual(origValue, retValue); + + // new Key value pair created with older value, the etag is reusing the existingnewkey etag + var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); + ClassicAssert.AreEqual(0, (long)res[0]); + ClassicAssert.AreEqual(origValue, res[1].ToString()); + + origValue = db.StringGet("key1"); + ClassicAssert.AreEqual(null, origValue); + } + + [Test] public void PersistTTLTestForEtagSetData() { From d581562af7286ed746b4edc7b49bfc2179a268c3 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 2 Oct 2024 12:34:06 +1000 Subject: [PATCH 17/87] remove shifting in network buffer in setifmatch --- libs/server/Resp/BasicCommands.cs | 17 +++--------- .../Storage/Functions/MainStore/RMWMethods.cs | 27 ++++++++++++++----- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ae78ea753e..e8bea6a0c2 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -366,25 +366,19 @@ private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) var key = parseState.GetArgSliceByRef(0).SpanByte; var value = parseState.GetArgSliceByRef(1).SpanByte; - // since the etag is a long, we now have a copy of it on the stack, and the underlying memory can be used as an extension for value's spanbyte later long etagToCheckWith = parseState.GetLong(2); /* - Here we make space for etag to be added infront of value. We borrow 8 bytes from infront of the value, we will later restore the memory for the location we borrow. P.s. This is NOT GOING TO create a buffer overflow becuase of the following reason. Value spanbyte points to the network buffer, the network buffer is already holding key, value, and etag in a contiguous chunk of memory, in order, along with padding for separators in Resp. This means there has to be ENOUGH OR MORE space for len(value) + sizeof(long). - So once we read the etag from the network buffer onto the stack, we can borrow 8 bytes of memory infront of value spanbyte safely, hence not creating a buffer overflow - when borrowing 8 bytes to shove the etag into the expanded spanbyte for value, which we then use as our input buffer. + All we are doing is borrowing the 8 bytes of memory infront of the value span and making it a part of the same spanbyte so we can essentially do the following transformation. + [] -> [] */ - byte* borrowedMemLocation = value.ToPointer() + sizeof(long); - long saved8Bytes = *(long*)borrowedMemLocation; + int initialSizeOfValueSpan = value.Length; value.Length = initialSizeOfValueSpan + sizeof(long); - // move contents of value 8 bytes forward - Buffer.MemoryCopy(value.ToPointer(), value.ToPointer() + sizeof(long), initialSizeOfValueSpan, initialSizeOfValueSpan); - // add the etag at first 8 bytes - *(long*)value.ToPointer() = etagToCheckWith; + *(long*)(value.ToPointer() + initialSizeOfValueSpan) = etagToCheckWith; // Make space for key header var keyPtr = key.ToPointer() - sizeof(int); @@ -394,9 +388,6 @@ P.s. This is NOT GOING TO create a buffer overflow becuase of the following reas // Here Etag retain argument does not really matter because setifmatch may or may not update etag based on the "if match" condition NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, true, ref storageApi); - // restore the 8 bytes we had messed with on the network buffer - *(long*)borrowedMemLocation = saved8Bytes; - return true; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 9e30a4713e..d71fa8cf60 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -276,7 +276,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span } long prevEtag = *(long*)value.ToPointer(); - long etagFromClient = *(long*)(inputPtr + RespInputHeader.Size); + + byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - sizeof (long); + long etagFromClient= *(long*)locationOfEtagInInputPtr; + if (prevEtag != etagFromClient) { // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH @@ -288,7 +291,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span if (input.Length - RespInputHeader.Size > value.Length) return false; // Increment the ETag - *(long*)(input.ToPointer() + RespInputHeader.Size) += 1; + long newEtag = prevEtag + 1; // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); @@ -297,7 +300,12 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span // Copy input to value value.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan()); + + *(long*)value.ToPointer() = newEtag; + input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - sizeof(long))[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); + + var debugCheck = value.ToPointer(); + rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1, true); @@ -626,7 +634,10 @@ public bool NeedCopyUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte ol return false; long existingEtag = *(long*)oldValue.ToPointer(); - long etagToCheckWith = *(long*)(input.ToPointer() + RespInputHeader.Size); + + byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - sizeof (long); + long etagToCheckWith = *(long*)locationOfEtagInInputPtr; + if (existingEtag != etagToCheckWith) { // cancellation and return false indicates ETag mismatch @@ -714,11 +725,15 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa // this update is so the early call to send the resp command works, outside of the switch // we are doing a double op of setting the etag to normalize etag update for other operations - *(long*)(input.ToPointer() + RespInputHeader.Size) += 1; + byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - sizeof (long); + long etagToCheckWith = *(long*)locationOfEtagInInputPtr; // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan()); + + *(long*)newValue.ToPointer() = etagToCheckWith + 1; + input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - sizeof(long))[RespInputHeader.Size..].CopyTo(newValue.AsSpan(sizeof(long))); + // Write Etag and Val back to Client CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1, true); From 355bef966c9159638fc3626ed7d6ef4a192ab91a Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 2 Oct 2024 12:42:37 +1000 Subject: [PATCH 18/87] Fix named parameters for readability --- libs/server/Resp/BasicCommands.cs | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index e8bea6a0c2..b317da87aa 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -386,7 +386,7 @@ P.s. This is NOT GOING TO create a buffer overflow becuase of the following reas *(int*)keyPtr = key.Length; // Here Etag retain argument does not really matter because setifmatch may or may not update etag based on the "if match" condition - NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, true, ref storageApi); + NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, getValue: true, highPrecision: false, retainEtag: true, ref storageApi); return true; } @@ -437,7 +437,7 @@ private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) *(int*)keyPtr = key.Length; // calling set with etag on an exisitng key will update the etag of the existing key - NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, true, false, retainEtag, ref storageApi); + NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, keyPtr, value.ToPointer() - sizeof(int), value.Length, getValue: true, highPrecision: false, retainEtag, ref storageApi); return true; } @@ -748,22 +748,22 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { // cannot do blind upsert if isEtagRetained return NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, getValue, - false, true, ref storageApi); + highPrecision: false, retainEtag: true, ref storageApi); } else { return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, - false, false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, false, + ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, getValue: true, + highPrecision: false, retainEtag: false, ref storageApi) + : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, highPrecision: false, ref storageApi); // Can perform a blind update } case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, keyPtr, valPtr, vSize, - getValue, isEtagRetained, false, ref storageApi); + getValue, highPrecision: false, isEtagRetained, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, - getValue, false, isEtagRetained, ref storageApi); + getValue, highPrecision: false, isEtagRetained, ref storageApi); } break; @@ -775,22 +775,22 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { // cannot do a blind update return NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, getValue, - true, true, ref storageApi); + highPrecision: true, retainEtag: true, ref storageApi); } else { return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, - true, false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, true, + ? NetworkSET_Conditional(RespCommand.SET, expiry, keyPtr, valPtr, vSize, getValue: true, + highPrecision: true, retainEtag: false, ref storageApi) + : NetworkSET_EX(RespCommand.SET, expiry, keyPtr, valPtr, vSize, highPrecision: true, ref storageApi); // Can perform a blind update } case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, keyPtr, valPtr, vSize, - getValue, true, isEtagRetained, ref storageApi); + getValue, highPrecision: true, isEtagRetained, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, - getValue, true, isEtagRetained, ref storageApi); + getValue, highPrecision: true, isEtagRetained, ref storageApi); } break; @@ -802,13 +802,13 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) case ExistOptions.None: // We can never perform a blind update due to KEEPTTL return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, keyPtr, valPtr, vSize, - getValue, false, isEtagRetained, ref storageApi); + getValue, highPrecision: false, isEtagRetained, ref storageApi); case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, keyPtr, valPtr, vSize, - getValue, false, isEtagRetained, ref storageApi); + getValue, highPrecision: false, isEtagRetained, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, keyPtr, valPtr, vSize, - getValue, false, isEtagRetained, ref storageApi); + getValue, highPrecision: false, isEtagRetained, ref storageApi); } break; From 8b18e43d5914a8749d56b82bf53b57cbfc03f397 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 2 Oct 2024 13:24:00 +1000 Subject: [PATCH 19/87] use constants for etag size --- libs/server/Constants.cs | 11 +++++++ libs/server/Resp/BasicCommands.cs | 4 +-- .../Storage/Functions/MainStore/RMWMethods.cs | 32 +++++++++---------- .../Functions/MainStore/ReadMethods.cs | 4 +-- .../Functions/MainStore/VarLenInputMethods.cs | 6 ++-- 5 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 libs/server/Constants.cs diff --git a/libs/server/Constants.cs b/libs/server/Constants.cs new file mode 100644 index 0000000000..f6e01df9af --- /dev/null +++ b/libs/server/Constants.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +namespace Garnet.server +{ + internal static class Constants + { + public const int EtagSize = sizeof(long); + } +} \ No newline at end of file diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index b317da87aa..9c36a0ea71 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -325,7 +325,7 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) // Setup input buffer to pass command info, and the ETag to check with. // len + header + etag's data type size - var inputSize = RespInputHeader.Size + sizeof(long) + sizeof(int); + var inputSize = RespInputHeader.Size + Constants.EtagSize + sizeof(int); SpanByte input = SpanByte.Reinterpret(stackalloc byte[inputSize]); byte* inputPtr = input.ToPointer(); ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETIFNOTMATCH; @@ -377,7 +377,7 @@ P.s. This is NOT GOING TO create a buffer overflow becuase of the following reas */ int initialSizeOfValueSpan = value.Length; - value.Length = initialSizeOfValueSpan + sizeof(long); + value.Length = initialSizeOfValueSpan + Constants.EtagSize; *(long*)(value.ToPointer() + initialSizeOfValueSpan) = etagToCheckWith; // Make space for key header diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index d71fa8cf60..9e589060d0 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -155,12 +155,12 @@ public bool InitialUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte va recordInfo.SetHasETag(); // Copy input to value - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + sizeof(long)); + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + Constants.EtagSize); value.ExtraMetadata = input.ExtraMetadata; // initial etag set to 0, this is a counter based etag that is incremented on change *(long*)value.ToPointer() = 0; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(Constants.EtagSize)); // Copy initial etag to output CopyRespNumber(0, ref output); @@ -250,7 +250,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span long oldEtag = -1; if (recordInfo.ETag) { - etagIgnoredOffset = sizeof(long); + etagIgnoredOffset = Constants.EtagSize; etagIgnoredEnd = value.LengthWithoutMetadata; oldEtag = *(long*)value.ToPointer(); } @@ -277,7 +277,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span long prevEtag = *(long*)value.ToPointer(); - byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - sizeof (long); + byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; long etagFromClient= *(long*)locationOfEtagInInputPtr; if (prevEtag != etagFromClient) @@ -302,9 +302,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span value.ExtraMetadata = input.ExtraMetadata; *(long*)value.ToPointer() = newEtag; - input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - sizeof(long))[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); - - var debugCheck = value.ToPointer(); + input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - Constants.EtagSize)[RespInputHeader.Size..].CopyTo(value.AsSpan(Constants.EtagSize)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); @@ -524,7 +522,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span return false; case RespCommand.SETWITHETAG: - if (input.Length - RespInputHeader.Size + sizeof(long) > value.Length) + if (input.Length - RespInputHeader.Size + Constants.EtagSize > value.Length) return false; // retain the older etag (and increment it to account for this update) if requested and if it also exists otherwise set etag to initial etag of 0 @@ -533,11 +531,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span recordInfo.SetHasETag(); // Copy input to value - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + sizeof(long)); + value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + Constants.EtagSize); value.ExtraMetadata = input.ExtraMetadata; *(long*)value.ToPointer() = etagVal; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(sizeof(long))); + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(Constants.EtagSize)); // Copy initial etag to output CopyRespNumber(etagVal, ref output); @@ -623,7 +621,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte ol int etagIgnoredEnd = -1; if (rmwInfo.RecordInfo.ETag) { - etagIgnoredOffset = sizeof(long); + etagIgnoredOffset = Constants.EtagSize; etagIgnoredEnd = oldValue.LengthWithoutMetadata; } @@ -635,7 +633,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref SpanByte input, ref SpanByte ol long existingEtag = *(long*)oldValue.ToPointer(); - byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - sizeof (long); + byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; long etagToCheckWith = *(long*)locationOfEtagInInputPtr; if (existingEtag != etagToCheckWith) @@ -696,7 +694,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa long oldEtag = -1; if (recordInfo.ETag) { - etagIgnoredOffset = sizeof(long); + etagIgnoredOffset = Constants.EtagSize; etagIgnoredEnd = oldValue.LengthWithoutMetadata; oldEtag = *(long*)oldValue.ToPointer(); } @@ -704,7 +702,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa switch (cmd) { case RespCommand.SETWITHETAG: - Debug.Assert(input.Length - RespInputHeader.Size + sizeof(long) == newValue.Length); + Debug.Assert(input.Length - RespInputHeader.Size + Constants.EtagSize == newValue.Length); // etag setting will be done here so does not need to be incremented outside switch shouldUpdateEtag = false; @@ -713,7 +711,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa recordInfo.SetHasETag(); // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(sizeof(long))); + input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(Constants.EtagSize)); // set the etag *(long*)newValue.ToPointer() = etagVal; // Copy initial etag to output @@ -725,14 +723,14 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa // this update is so the early call to send the resp command works, outside of the switch // we are doing a double op of setting the etag to normalize etag update for other operations - byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - sizeof (long); + byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; long etagToCheckWith = *(long*)locationOfEtagInInputPtr; // Copy input to value newValue.ExtraMetadata = input.ExtraMetadata; *(long*)newValue.ToPointer() = etagToCheckWith + 1; - input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - sizeof(long))[RespInputHeader.Size..].CopyTo(newValue.AsSpan(sizeof(long))); + input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - Constants.EtagSize)[RespInputHeader.Size..].CopyTo(newValue.AsSpan(Constants.EtagSize)); // Write Etag and Val back to Client diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 945b258079..f0d2a82757 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -49,7 +49,7 @@ public bool SingleReader(ref SpanByte key, ref SpanByte input, ref SpanByte valu var end = -1; if (!isEtagCmd && readInfo.RecordInfo.ETag) { - start = sizeof(long); + start = Constants.EtagSize; end = value.LengthWithoutMetadata; } @@ -102,7 +102,7 @@ public bool ConcurrentReader(ref SpanByte key, ref SpanByte input, ref SpanByte var end = -1; if (!isEtagCmd && recordInfo.ETag) { - start = sizeof(long); + start = Constants.EtagSize; end = value.LengthWithoutMetadata; } diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 1b3836bfb5..3f6b46b820 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -86,7 +86,7 @@ public int GetRMWInitialValueLength(ref SpanByte input) return sizeof(int) + ndigits + (fNeg ? 1 : 0); case RespCommand.SETWITHETAG: // same space as SET but with 8 additional bytes for etag at the front of the payload - return sizeof(int) + input.Length - RespInputHeader.Size + sizeof(long); + return sizeof(int) + input.Length - RespInputHeader.Size + Constants.EtagSize; default: if (cmd >= 200) { @@ -112,7 +112,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool ha var inputspan = input.AsSpan(); var inputPtr = input.ToPointer(); var cmd = inputspan[0]; - int etagOffset = hasEtag ? sizeof(long) : 0; + int etagOffset = hasEtag ? Constants.EtagSize : 0; bool retainEtag = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag(); switch ((RespCommand)cmd) @@ -181,7 +181,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool ha break; case RespCommand.SETWITHETAG: // same space as SET but with 8 additional bytes for etag at the front of the payload - return sizeof(int) + input.Length - RespInputHeader.Size + sizeof(long); + return sizeof(int) + input.Length - RespInputHeader.Size + Constants.EtagSize; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: return sizeof(int) + t.Length + input.MetadataSize; From 0c340c88d86ab3f965bc5d6358ac9c032b3ecc07 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 2 Oct 2024 13:36:00 +1000 Subject: [PATCH 20/87] update read nil to be a faster --- libs/common/RespReadUtils.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/common/RespReadUtils.cs b/libs/common/RespReadUtils.cs index cbb388bf34..4244714a62 100644 --- a/libs/common/RespReadUtils.cs +++ b/libs/common/RespReadUtils.cs @@ -394,10 +394,11 @@ public static bool ReadNil(ref byte* ptr, byte* end, out byte? unexpectedToken) return false; } - ReadOnlySpan ptrNext5Bytes = new ReadOnlySpan(ptr, 5); - ReadOnlySpan expectedNilRepr = "$-1\r\n"u8; - if (!ptrNext5Bytes.SequenceEqual(expectedNilRepr)) + + if (*(uint*)(ptr) != MemoryMarshal.Read("$-1\r"u8) || *(byte*)(ptr + 4) != (byte)'\n') { + ReadOnlySpan ptrNext5Bytes = new ReadOnlySpan(ptr, 5); + ReadOnlySpan expectedNilRepr = "$-1\r\n"u8; for (int i = 0; i < 5; i++) { // first place where the sequence differs we have found the unexpected token From 141c8709ba2c065634cc922fd07d6d882fe8eeb4 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 2 Oct 2024 13:39:25 +1000 Subject: [PATCH 21/87] format --- libs/server/Resp/BasicCommands.cs | 2 +- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 9c36a0ea71..1776ce941b 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -375,7 +375,7 @@ P.s. This is NOT GOING TO create a buffer overflow becuase of the following reas All we are doing is borrowing the 8 bytes of memory infront of the value span and making it a part of the same spanbyte so we can essentially do the following transformation. [] -> [] */ - + int initialSizeOfValueSpan = value.Length; value.Length = initialSizeOfValueSpan + Constants.EtagSize; *(long*)(value.ToPointer() + initialSizeOfValueSpan) = etagToCheckWith; diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 9e589060d0..1a63fed8e2 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -278,7 +278,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span long prevEtag = *(long*)value.ToPointer(); byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; - long etagFromClient= *(long*)locationOfEtagInInputPtr; + long etagFromClient = *(long*)locationOfEtagInInputPtr; if (prevEtag != etagFromClient) { From a7756c8c145c5461c8710abbb9fb8892f248b369 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 7 Oct 2024 18:12:51 -0700 Subject: [PATCH 22/87] pr feedback --- libs/common/RespReadUtils.cs | 4 +- libs/server/Resp/BasicCommands.cs | 14 +- .../Functions/MainStore/PrivateMethods.cs | 6 +- .../Storage/Functions/MainStore/RMWMethods.cs | 3 +- .../Functions/MainStore/UpsertMethods.cs | 6 +- .../Storage/Session/MainStore/MainStoreOps.cs | 263 ++++++++++-------- test/Garnet.test/RespEtagTests.cs | 34 ++- 7 files changed, 195 insertions(+), 135 deletions(-) diff --git a/libs/common/RespReadUtils.cs b/libs/common/RespReadUtils.cs index 4244714a62..5aac63b27a 100644 --- a/libs/common/RespReadUtils.cs +++ b/libs/common/RespReadUtils.cs @@ -394,11 +394,11 @@ public static bool ReadNil(ref byte* ptr, byte* end, out byte? unexpectedToken) return false; } + ReadOnlySpan expectedNilRepr = "$-1\r\n"u8; - if (*(uint*)(ptr) != MemoryMarshal.Read("$-1\r"u8) || *(byte*)(ptr + 4) != (byte)'\n') + if (*(uint*)ptr != MemoryMarshal.Read(expectedNilRepr.Slice(0, 4)) || *(ptr + 4) != expectedNilRepr[4]) { ReadOnlySpan ptrNext5Bytes = new ReadOnlySpan(ptr, 5); - ReadOnlySpan expectedNilRepr = "$-1\r\n"u8; for (int i = 0; i < 5; i++) { // first place where the sequence differs we have found the unexpected token diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 1776ce941b..80c7fd486f 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -369,11 +369,9 @@ private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) long etagToCheckWith = parseState.GetLong(2); /* - P.s. This is NOT GOING TO create a buffer overflow becuase of the following reason. - Value spanbyte points to the network buffer, the network buffer is already holding key, value, and etag in a contiguous chunk of memory, in order, along with padding - for separators in Resp. This means there has to be ENOUGH OR MORE space for len(value) + sizeof(long). - All we are doing is borrowing the 8 bytes of memory infront of the value span and making it a part of the same spanbyte so we can essentially do the following transformation. - [] -> [] + The network buffer holds key, value, and etag in a contiguous chunk of memory, in order, along with padding for separators in RESP. + Shift the etag down over the post-value padding to immediately follow the value: + [] -> [] */ int initialSizeOfValueSpan = value.Length; @@ -832,6 +830,8 @@ private bool NetworkSET_EX(RespCommand cmd, int expiry, byte* keyPtr else { // Move payload forward to make space for metadata + // We can move the payload safely forward since for these commands value is followed by "$2\r\nEX\r\n" + // where we instead of EX we couldnt have XX|NX so we have 8 bytes we can borrow from Buffer.MemoryCopy(valPtr + sizeof(int), valPtr + sizeof(int) + sizeof(long), vsize, vsize); *(int*)valPtr = vsize + sizeof(long); SpanByte.Reinterpret(valPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + @@ -865,7 +865,9 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, byt } else { - // Move payload forward to make space for metadata + // Move payload forward to make space for metadata. + // We can move the payload safely forward since for these commands value is followed by "$2\r\nEX\r\n" + // where we instead of EX we couldnt have XX|NX so we have 8 bytes we can borrow from Buffer.MemoryCopy(inputPtr + sizeof(int) + RespInputHeader.Size, inputPtr + sizeof(int) + sizeof(long) + RespInputHeader.Size, isize, isize); *(int*)inputPtr = sizeof(long) + RespInputHeader.Size + isize; diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index d8a6b2bace..50fb16a8f3 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -591,13 +591,13 @@ void CopyRespNumber(long number, ref SpanByteAndMemory dst) /// /// Copy length of value to output (as ASCII bytes) /// - static void CopyValueLengthToOutput(ref SpanByte value, ref SpanByteAndMemory output, int adjustForEtagLen) + static void CopyValueLengthToOutput(ref SpanByte value, ref SpanByteAndMemory output, int eTagIgnoredOffset) { - int numDigits = NumUtils.NumDigits(value.LengthWithoutMetadata - adjustForEtagLen); + int numDigits = NumUtils.NumDigits(value.LengthWithoutMetadata - eTagIgnoredOffset); Debug.Assert(output.IsSpanByte, "This code assumes it is called in a non-pending context or in a pending context where dst.SpanByte's pointer remains valid"); var outputPtr = output.SpanByte.ToPointer(); - NumUtils.IntToBytes(value.LengthWithoutMetadata - adjustForEtagLen, numDigits, ref outputPtr); + NumUtils.IntToBytes(value.LengthWithoutMetadata - eTagIgnoredOffset, numDigits, ref outputPtr); output.SpanByte.Length = numDigits; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 1a63fed8e2..e69a28beb0 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -288,7 +288,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span } // Need CU if no space for new value - if (input.Length - RespInputHeader.Size > value.Length) return false; + if (input.Length - RespInputHeader.Size > value.Length) + return false; // Increment the ETag long newEtag = prevEtag + 1; diff --git a/libs/server/Storage/Functions/MainStore/UpsertMethods.cs b/libs/server/Storage/Functions/MainStore/UpsertMethods.cs index d46ef4a9cd..cd1fec8bf8 100644 --- a/libs/server/Storage/Functions/MainStore/UpsertMethods.cs +++ b/libs/server/Storage/Functions/MainStore/UpsertMethods.cs @@ -12,7 +12,10 @@ namespace Garnet.server { /// public bool SingleWriter(ref SpanByte key, ref SpanByte input, ref SpanByte src, ref SpanByte dst, ref SpanByteAndMemory output, ref UpsertInfo upsertInfo, WriteReason reason, ref RecordInfo recordInfo) - => SpanByteFunctions.DoSafeCopy(ref src, ref dst, ref upsertInfo, ref recordInfo); + { + recordInfo.ClearHasETag(); + return SpanByteFunctions.DoSafeCopy(ref src, ref dst, ref upsertInfo, ref recordInfo); + } /// public void PostSingleWriter(ref SpanByte key, ref SpanByte input, ref SpanByte src, ref SpanByte dst, ref SpanByteAndMemory output, ref UpsertInfo upsertInfo, WriteReason reason) @@ -25,6 +28,7 @@ public void PostSingleWriter(ref SpanByte key, ref SpanByte input, ref SpanByte /// public bool ConcurrentWriter(ref SpanByte key, ref SpanByte input, ref SpanByte src, ref SpanByte dst, ref SpanByteAndMemory output, ref UpsertInfo upsertInfo, ref RecordInfo recordInfo) { + recordInfo.ClearHasETag(); if (ConcurrentWriterWorker(ref src, ref dst, ref upsertInfo, ref recordInfo)) { if (!upsertInfo.RecordInfo.Modified) diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index f4bd4e72c4..b57dfe651f 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -597,43 +597,54 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S { try { - SpanByte input = default; - var o = new SpanByteAndMemory(); - var status = GET(ref oldKey, ref input, ref o, ref context); + // GET with etag to find if the key alrady exists, and if it exists then we can check if it also has an etag + var inputSize = sizeof(int) + RespInputHeader.Size; + SpanByte getWithEtagInput = SpanByte.Reinterpret(stackalloc byte[inputSize]); + byte* inputPtr = getWithEtagInput.ToPointer(); + ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; + ((RespInputHeader*)inputPtr)->flags = 0; + + SpanByteAndMemory etagAndDataOutput = new SpanByteAndMemory(); + GarnetStatus status = GET(ref oldKey, ref getWithEtagInput, ref etagAndDataOutput, ref context); if (status == GarnetStatus.OK) { - // since we didn't give the output span any memory when creating it, the backend would necessarily have had to allocate heap memory if item is not NOTFOUND - Debug.Assert(!o.IsSpanByte); - var memoryHandle = o.Memory.Memory.Pin(); - var ptrVal = (byte*)memoryHandle.Pointer; - - RespReadUtils.ReadUnsignedLengthHeader(out var headerLength, ref ptrVal, ptrVal + o.Length); - - // Find expiration time of the old key - var expireSpan = new SpanByteAndMemory(); - var ttlStatus = TTL(ref oldKey, storeType, ref expireSpan, ref context, ref objectContext, true); - - // Find if this is ETag based key - SpanByte getWithEtagInput = SpanByte.Reinterpret(stackalloc byte[NumUtils.MaximumFormatInt64Length]); - byte* inputPtr = getWithEtagInput.ToPointer(); - ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; - ((RespInputHeader*)inputPtr)->flags = 0; - // Check if etag is nil for getWithEtagStatus, this tells us if the key already had an etag associated with it bool hasEtag = false; - SpanByteAndMemory etagAndDataOutput = new SpanByteAndMemory(); - GarnetStatus getWithEtagStatus = GET(ref oldKey, ref getWithEtagInput, ref etagAndDataOutput, ref context); - // we know this key exists, so there is no reason for the status to not be OK - Debug.Assert(getWithEtagStatus == GarnetStatus.OK); + // since we didn't give the etagAndDataOutput span any memory when creating it, the backend would necessarily have had to allocate heap memory if item is not NOTFOUND Debug.Assert(!etagAndDataOutput.IsSpanByte); - MemoryHandle outputMemHandle = etagAndDataOutput.Memory.Memory.Pin(); + using MemoryHandle outputMemHandle = etagAndDataOutput.Memory.Memory.Pin(); byte* outputBufCurr = (byte*)outputMemHandle.Pointer; byte* end = outputBufCurr + etagAndDataOutput.Length; + + // GETWITHETAG returns an array of two items, etag, and value. + // we need to read past RESP metadata and control sequences to get to etag, and value RespReadUtils.ReadUnsignedArrayLength(out int numItemInArr, ref outputBufCurr, end); - Debug.Assert(numItemInArr == 2); - hasEtag = !RespReadUtils.ReadNil(ref outputBufCurr, end, out byte? _); + + Debug.Assert(numItemInArr == 2, "GETWITHETAG output RESP array should be of 2 elements only."); + + // we know a key-val pair does not have an etag if the first element is not null + // if read nil is successful it will point to the ptrVal otherwise we need to re-read past the etag as RESP int64 + byte* startOfEtagPtr = outputBufCurr; + hasEtag = !RespReadUtils.ReadNil(ref outputBufCurr, end, out _); + if (hasEtag) + { + // read past the etag so we can then get to the ptrVal, we don't need specific val of etag, just need to move past it if it exists + bool etagReadingSuccessful = RespReadUtils.Read64Int(out _, ref startOfEtagPtr, end); + Debug.Assert(etagReadingSuccessful, "Etag should have been read succesffuly"); + // now startOfEtagPtr is past the etag and points to value + outputBufCurr = startOfEtagPtr; + } + + // get length of value from the header + RespReadUtils.ReadUnsignedLengthHeader(out int headerLength, ref outputBufCurr, end); + // outputBuf now points to start of value + byte* ptrVal = outputBufCurr; + + // Find expiration time of the old key + var expireSpan = new SpanByteAndMemory(); + var ttlStatus = TTL(ref oldKey, storeType, ref expireSpan, ref context, ref objectContext, true); if (ttlStatus == GarnetStatus.OK && !expireSpan.IsSpanByte) { @@ -642,51 +653,54 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _); // If the key has an expiration, set the new key with the expiration - if (expireTimeMs > 0 && !hasEtag) - { - if (isNX) - { - // Move payload forward to make space for RespInputHeader and Metadata - var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength)); - var setValueSpan = setValue.SpanByte; - var setValuePtr = setValueSpan.ToPointerWithMetadata(); - setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; - ((RespInputHeader*)(setValuePtr + sizeof(long)))->cmd = RespCommand.SETEXNX; - ((RespInputHeader*)(setValuePtr + sizeof(long)))->flags = 0; - var newKey = newKeySlice.SpanByte; - var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context); - - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - returnStatus = GarnetStatus.OK; - } - else - { - SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context); - } - } - else if (expireTimeMs == -1 && !hasEtag) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key + if (!hasEtag) { - if (isNX) + if (expireTimeMs > 0) { - // Move payload forward to make space for RespInputHeader - var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size, new ArgSlice(ptrVal, headerLength)); - var setValueSpan = setValue.SpanByte; - var setValuePtr = setValueSpan.ToPointerWithMetadata(); - ((RespInputHeader*)setValuePtr)->cmd = RespCommand.SETEXNX; - ((RespInputHeader*)setValuePtr)->flags = 0; - var newKey = newKeySlice.SpanByte; - var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context); - - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - returnStatus = GarnetStatus.OK; + if (isNX) + { + // Move payload forward to make space for RespInputHeader and Metadata + var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength)); + var setValueSpan = setValue.SpanByte; + var setValuePtr = setValueSpan.ToPointerWithMetadata(); + setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; + ((RespInputHeader*)(setValuePtr + sizeof(long)))->cmd = RespCommand.SETEXNX; + ((RespInputHeader*)(setValuePtr + sizeof(long)))->flags = 0; + var newKey = newKeySlice.SpanByte; + var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context); + + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; + returnStatus = GarnetStatus.OK; + } + else + { + SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context); + } } - else + else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key { - SpanByte newKey = newKeySlice.SpanByte; - var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); - SET(ref newKey, ref value, ref context); + if (isNX) + { + // Move payload forward to make space for RespInputHeader + var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size, new ArgSlice(ptrVal, headerLength)); + var setValueSpan = setValue.SpanByte; + var setValuePtr = setValueSpan.ToPointerWithMetadata(); + ((RespInputHeader*)setValuePtr)->cmd = RespCommand.SETEXNX; + ((RespInputHeader*)setValuePtr)->flags = 0; + var newKey = newKeySlice.SpanByte; + var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context); + + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; + returnStatus = GarnetStatus.OK; + } + else + { + SpanByte newKey = newKeySlice.SpanByte; + var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); + SET(ref newKey, ref value, ref context); + } } } else if ( @@ -695,68 +709,75 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S { // IsNX means if the newKey Exists do not set it // We can't use SET with not exist here so instead we will do an Exists and skip the seeting if exists - if (isNX && EXISTS(newKeySlice, storeType, ref context, ref objectContext) == GarnetStatus.OK) + bool newKeyAlreadyExists = EXISTS(newKeySlice, storeType, ref context, ref objectContext) == GarnetStatus.OK; + + if (isNX && newKeyAlreadyExists) { + // Skip setting the new key and go to calling the part after that result = 0; returnStatus = GarnetStatus.OK; - // Skip setting the new key and go to calling the part after that - goto AFTERNEWKEYSET; - } - - if (isNX) - result = 1; - - SpanByte newKey = newKeySlice.SpanByte; - - var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); - var initialValueSize = value.Length; - - var valPtr = value.ToPointer(); - - SpanByte key = newKeySlice.SpanByte; - - // Make space for key header - var keyPtr = key.ToPointer() - sizeof(int); - // Set key length - *(int*)keyPtr = key.Length; - - // Make space for resp input header - valPtr -= (RespInputHeader.Size + sizeof(int)); - if (expireTimeMs == -1) // no expiration provided - { - *(int*)valPtr = RespInputHeader.Size + value.Length; - ((RespInputHeader*)(valPtr + sizeof(int)))->cmd = RespCommand.SETWITHETAG; - ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; - // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag - // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key - ((RespInputHeader*)(valPtr + sizeof(int)))->SetRetainEtagFlag(); } else { - // Move payload forward to make space for metadata - Buffer.MemoryCopy(valPtr + sizeof(int) + RespInputHeader.Size, - valPtr + sizeof(int) + sizeof(long) + RespInputHeader.Size, value.Length, value.Length); - *(int*)valPtr = sizeof(long) + RespInputHeader.Size + value.Length; - ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->cmd = RespCommand.SETWITHETAG; - ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->flags = 0; - // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag - // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key - ((RespInputHeader*)(valPtr + sizeof(int) + sizeof(long)))->SetRetainEtagFlag(); - - SpanByte.Reinterpret(inputPtr).ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; - } + var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); + var initialValueSize = value.Length; + + var valPtr = value.ToPointer(); + + SpanByte key = newKeySlice.SpanByte; + + GarnetStatus setStatus; + if (expireTimeMs == -1) // no expiration provided + { + /* + * Make Space for Resp Input Header Behind The valPtr: + * This will not underflow because SpanByte is being created from pinnned pointer on output buffer that had etag and control sequences behind ptrVal. + * we need 6 bytes behind valPtr that we know exists because even in worst case where we only an etag of 0 (1 byte in resp output buffer), the output + * buffer will be of structure: + * *2\r\n + * :0\r\n + * <_VALUE_>\r\n + * this gives us 6 bytes behind it in pinned memory that we can borrow, and 2 bytes infront of value + */ + valPtr -= RespInputHeader.Size + sizeof(int); + // set the length + *(int*)valPtr = RespInputHeader.Size + value.Length; + ((RespInputHeader*)(valPtr + sizeof(int)))->cmd = RespCommand.SETWITHETAG; + ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; + // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag + // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key + ((RespInputHeader*)(valPtr + sizeof(int)))->SetRetainEtagFlag(); + + setStatus = SET_Conditional(ref key, ref Unsafe.AsRef(valPtr), ref context); + } + else + { + // make space for metadata to be added to valPtr + var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength)); + var setValueSpan = setValue.SpanByte; + var setValuePtr = setValueSpan.ToPointerWithMetadata(); + ((RespInputHeader*)setValuePtr + sizeof(long))->cmd = RespCommand.SETWITHETAG; + ((RespInputHeader*)setValuePtr + sizeof(long))->flags = 0; + // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag + // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key + ((RespInputHeader*)setValuePtr)->SetRetainEtagFlag(); + + setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; + + setStatus = SET_Conditional(ref key, ref setValueSpan, ref context); + } + + + // isNx or/and new key does not exist, either way we can set result to 1 result var will only get used if !isNx, and otherwise is ignored. + // Setting result regardless will avoid the need to add branching here + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - SET_Conditional(ref Unsafe.AsRef(keyPtr), - ref Unsafe.AsRef(valPtr), ref context); - returnStatus = GarnetStatus.OK; + returnStatus = GarnetStatus.OK; + } } - - AFTERNEWKEYSET: - - outputMemHandle.Dispose(); + etagAndDataOutput.Memory.Dispose(); expireSpan.Memory.Dispose(); - memoryHandle.Dispose(); - o.Memory.Dispose(); // Delete the old key only when SET NX succeeded if (isNX && result == 1) diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 2ff249dc4a..6501c2e111 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1209,7 +1209,7 @@ public void SingleRenameShouldAddEtagIfOldKeyHadEtagButNotExistingNewkey() string retValue = db.StringGet(existingNewKey); ClassicAssert.AreEqual(origValue, retValue); - // new Key value pair created with older value, the etag is reusing the existingnewkey etag + // new Key value pair created with older value var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); ClassicAssert.AreEqual(0, (long)res[0]); ClassicAssert.AreEqual(origValue, res[1].ToString()); @@ -1218,6 +1218,38 @@ public void SingleRenameShouldAddEtagIfOldKeyHadEtagButNotExistingNewkey() ClassicAssert.AreEqual(null, origValue); } + [Test] + public void SingleRenameShouldAddEtagAndMetadataIfOldKeyHadEtagAndMetadata() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + string origKey = "key1"; + string origValue = "test1"; + long etag = (long)db.Execute("SETWITHETAG", ["key1", origValue]); + ClassicAssert.AreEqual(0, etag); + + ClassicAssert.IsTrue(db.KeyExpire(origKey, TimeSpan.FromSeconds(10))); + + string newKey = "key2"; + db.KeyRename(origKey, newKey); + + string retValue = db.StringGet(newKey); + ClassicAssert.AreEqual(origValue, retValue); + + // new Key value pair created with older value + var res = (RedisResult[])db.Execute("GETWITHETAG", [newKey]); + ClassicAssert.AreEqual(0, (long)res[0]); + ClassicAssert.AreEqual(origValue, res[1].ToString()); + + // check that the ttl is not empty on new key because it inherited it from prev key + TimeSpan? ttl = db.KeyTimeToLive(newKey); + ClassicAssert.IsNotNull(ttl); + + origValue = db.StringGet(origKey); + ClassicAssert.AreEqual(null, origValue); + } + [Test] public void PersistTTLTestForEtagSetData() From 80142427818ae0061b7906ea7baa7a2e62db77c5 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 7 Oct 2024 19:05:01 -0700 Subject: [PATCH 23/87] remove redundant ternary --- libs/server/Storage/Session/MainStore/MainStoreOps.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 2dfd6e05a3..715fa905b4 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -443,11 +443,10 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref SpanB incr_session_notfound(); return GarnetStatus.NOTFOUND; } - else if (cmd == RespCommand.SETIFMATCH && !status.IsUpdated) + else if (cmd == RespCommand.SETIFMATCH && status.IsCanceled) { // The RMW operation for SETIFMATCH upon not finding the etags match between the existing record and sent etag returns Cancelled Operation - incr_session_found(); - return status.IsCanceled ? GarnetStatus.ETAGMISMATCH : GarnetStatus.WRONGTYPE; + return GarnetStatus.ETAGMISMATCH; } else { From abc6d8f9055ae20d11fec4bab4c16afcc3ef3df7 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 9 Oct 2024 14:48:30 -0700 Subject: [PATCH 24/87] update tests for transaction --- libs/resources/RespCommandsInfo.json | 8 +-- .../Functions/MainStore/PrivateMethods.cs | 1 - libs/server/Transaction/TxnKeyManager.cs | 4 ++ test/Garnet.test/RespEtagTests.cs | 8 +++ test/Garnet.test/TransactionTests.cs | 65 ++++++++++++++++++- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index d3d4d6a354..158de1e6e6 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -1355,7 +1355,7 @@ "Command": "GETIFNOTMATCH", "Name": "GETIFNOTMATCH", "IsInternal": false, - "Arity": 2, + "Arity": 3, "Flags": "NONE", "FirstKey": 1, "LastKey": 1, @@ -1394,7 +1394,7 @@ "Command": "GETWITHETAG", "Name": "GETWITHETAG", "IsInternal": false, - "Arity": 1, + "Arity": 2, "Flags": "NONE", "FirstKey": 1, "LastKey": 1, @@ -3244,7 +3244,7 @@ "Command": "SETIFMATCH", "Name": "SETIFMATCH", "IsInternal": false, - "Arity": 3, + "Arity": 4, "Flags": "NONE", "FirstKey": 1, "LastKey": 1, @@ -3283,7 +3283,7 @@ "Command": "SETWITHETAG", "Name": "SETWITHETAG", "IsInternal": false, - "Arity": 2, + "Arity": -3, "Flags": "NONE", "FirstKey": 1, "LastKey": 1, diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 88702cffe2..91a79d7d39 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -299,7 +299,6 @@ void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end) { // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element - var initPtr = curr; RespWriteUtils.WriteArrayLength(2, ref curr, end); if (etag == -1) { diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index 0dec851736..12f4797369 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -120,7 +120,11 @@ internal int GetKeys(RespCommand command, int inputCount, out ReadOnlySpan RespCommand.HSTRLEN => HashObjectKeys((byte)HashOperation.HSTRLEN), RespCommand.HVALS => HashObjectKeys((byte)HashOperation.HVALS), RespCommand.GET => SingleKey(1, false, LockType.Shared), + RespCommand.GETIFNOTMATCH => SingleKey(1, false, LockType.Shared), + RespCommand.GETWITHETAG => SingleKey(1, false, LockType.Shared), RespCommand.SET => SingleKey(1, false, LockType.Exclusive), + RespCommand.SETWITHETAG => SingleKey(1, false, LockType.Exclusive), + RespCommand.SETIFMATCH => SingleKey(1, false, LockType.Exclusive), RespCommand.GETRANGE => SingleKey(1, false, LockType.Shared), RespCommand.SETRANGE => SingleKey(1, false, LockType.Exclusive), RespCommand.PFADD => SingleKey(1, false, LockType.Exclusive), diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 6501c2e111..17bd45e58a 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1907,6 +1907,10 @@ public void SetBitOperationsOnEtagSetData() setbits = db.StringBitCount(key); ClassicAssert.AreEqual(expectedBitCount, setbits); + // Use BitPosition to find the first set bit + long firstSetBitPosition = db.StringBitPosition(key, true); + ClassicAssert.AreEqual(0, firstSetBitPosition); // As we are setting bits in order, first set bit should be 0 + // with each bit set that we do, we are increasing the etag as well by 1 etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); ClassicAssert.AreEqual(expectedBitCount, etagToCheck); @@ -1928,6 +1932,10 @@ public void SetBitOperationsOnEtagSetData() setbits = db.StringBitCount(key); ClassicAssert.AreEqual(expectedBitCount, setbits); + // Use BitPosition to find the first unset bit + long firstUnsetBitPosition = db.StringBitPosition(key, false); + ClassicAssert.AreEqual(i, firstUnsetBitPosition); // After unsetting, the first unset bit should be i + etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); ClassicAssert.AreEqual(expectedEtag, etagToCheck); } diff --git a/test/Garnet.test/TransactionTests.cs b/test/Garnet.test/TransactionTests.cs index 76e91799b1..0fde102b8f 100644 --- a/test/Garnet.test/TransactionTests.cs +++ b/test/Garnet.test/TransactionTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Text; using System.Threading.Tasks; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -212,10 +213,66 @@ public async Task SimpleWatchTest() res = lightClientRequest.SendCommand("EXEC"); expectedResponse = "*2\r\n$14\r\nvalue1_updated\r\n+OK\r\n"; - ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + ClassicAssert.AreEqual( + expectedResponse, + Encoding.ASCII.GetString(res.AsSpan().Slice(0, expectedResponse.Length))); } + [Test] + public async Task WatchTestWithSetWithEtag() + { + var lightClientRequest = TestUtils.CreateRequest(); + byte[] res; + + string expectedResponse = ":0\r\n"; + res = lightClientRequest.SendCommand("SETWITHETAG key1 value1"); + ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + + expectedResponse = "+OK\r\n"; + res = lightClientRequest.SendCommand("WATCH key1"); + ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + + res = lightClientRequest.SendCommand("MULTI"); + ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + + res = lightClientRequest.SendCommand("GET key1"); + expectedResponse = "+QUEUED\r\n"; + ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + res = lightClientRequest.SendCommand("SET key2 value2"); + ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + + await Task.Run(() => updateKey("key1", "value1_updated", retainEtag: true)); + + res = lightClientRequest.SendCommand("EXEC"); + expectedResponse = "*-1"; + ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); + + // This one should Commit + lightClientRequest.SendCommand("MULTI"); + + lightClientRequest.SendCommand("GET key1"); + lightClientRequest.SendCommand("SET key2 value2"); + // check that all the etag commands can be called inside a transaction + lightClientRequest.SendCommand("SETWITHETAG key3 value2"); + lightClientRequest.SendCommand("GETWITHETAG key3"); + lightClientRequest.SendCommand("GETIFNOTMATCH key3 0"); + lightClientRequest.SendCommand("SETIFMATCH key3 anotherVal 0"); + lightClientRequest.SendCommand("SETWITHETAG key3 arandomval RETAINETAG"); + + res = lightClientRequest.SendCommand("EXEC"); + + expectedResponse = "*7\r\n$14\r\nvalue1_updated\r\n+OK\r\n:0\r\n*2\r\n:0\r\n$6\r\nvalue2\r\n+NOTCHANGED\r\n*2\r\n:1\r\n$10\r\nanotherVal\r\n:2\r\n"; + string response = Encoding.ASCII.GetString(res.AsSpan().Slice(0, expectedResponse.Length)); + ClassicAssert.AreEqual(response, expectedResponse); + + // check if we still have the appropriate etag on the key we had set + var otherLighClientRequest = TestUtils.CreateRequest(); + res = otherLighClientRequest.SendCommand("GETWITHETAG key1"); + expectedResponse = "*2\r\n:1\r\n$14\r\nvalue1_updated\r\n"; + response = Encoding.ASCII.GetString(res.AsSpan().Slice(0, expectedResponse.Length)); + ClassicAssert.AreEqual(response, expectedResponse); + } [Test] public async Task WatchNonExistentKey() @@ -304,10 +361,12 @@ public async Task WatchKeyFromDisk() ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); } - private static void updateKey(string key, string value) + private static void updateKey(string key, string value, bool retainEtag = false) { using var lightClientRequest = TestUtils.CreateRequest(); - byte[] res = lightClientRequest.SendCommand("SET " + key + " " + value); + string command = $"SET {key} {value}"; + command += retainEtag ? " RETAINETAG" : ""; + byte[] res = lightClientRequest.SendCommand(command); string expectedResponse = "+OK\r\n"; ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); } From 802ff7358a12c15bbec5e68b39d8728d42a3d468 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 10 Oct 2024 16:42:07 -0700 Subject: [PATCH 25/87] Add test to make sure etag data works with recovery scenarios --- test/Garnet.test/RespAofTests.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/Garnet.test/RespAofTests.cs b/test/Garnet.test/RespAofTests.cs index 76271caea1..4ebf3d7c7c 100644 --- a/test/Garnet.test/RespAofTests.cs +++ b/test/Garnet.test/RespAofTests.cs @@ -229,6 +229,8 @@ public void AofRMWStoreRecoverTest() var db = redis.GetDatabase(0); db.StringSet("SeAofUpsertRecoverTestKey1", "SeAofUpsertRecoverTestValue1", expiry: TimeSpan.FromDays(1), when: When.NotExists); db.StringSet("SeAofUpsertRecoverTestKey2", "SeAofUpsertRecoverTestValue2", expiry: TimeSpan.FromDays(1), when: When.NotExists); + db.Execute("SETWITHETAG", "SeAofUpsertRecoverTestKey3", "SeAofUpsertRecoverTestValue3"); + db.Execute("SETIFMATCH", "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", "0"); } server.Store.CommitAOF(true); @@ -243,6 +245,7 @@ public void AofRMWStoreRecoverTest() ClassicAssert.AreEqual("SeAofUpsertRecoverTestValue1", recoveredValue.ToString()); recoveredValue = db.StringGet("SeAofUpsertRecoverTestKey2"); ClassicAssert.AreEqual("SeAofUpsertRecoverTestValue2", recoveredValue.ToString()); + ExpectedEtagTest(db, "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", 1); } } @@ -647,5 +650,26 @@ public void AofListObjectStoreRecoverTest() ClassicAssert.AreEqual(ldata, returnedData); } } + + private static void ExpectedEtagTest(IDatabase db, string key, string expectedValue, long expected) + { + RedisResult res = db.Execute("GETWITHETAG", key); + if (expectedValue == null) + { + ClassicAssert.IsTrue(res.IsNull); + return; + } + + RedisResult[] etagAndVal = (RedisResult[])res; + RedisResult etag = etagAndVal[0]; + RedisResult val = etagAndVal[1]; + + if (expected == -1) + { + ClassicAssert.IsTrue(etag.IsNull); + } + + ClassicAssert.AreEqual(expectedValue, val.ToString()); + } } } \ No newline at end of file From 8bda79a34b87cc5204b1b4f7f9a0c692d845e947 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 23 Oct 2024 17:36:43 -0700 Subject: [PATCH 26/87] WIP --- .../server/Resp/Bitmap/BitmapManagerBitPos.cs | 36 +++++++++----- .../Functions/MainStore/PrivateMethods.cs | 33 +------------ .../Storage/Functions/MainStore/RMWMethods.cs | 6 ++- test/Garnet.test/GarnetBitmapTests.cs | 27 +++++++++-- test/Garnet.test/RespEtagTests.cs | 47 +++++++++++++++++++ 5 files changed, 98 insertions(+), 51 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index 7e002e48e4..0f5246a85d 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -141,7 +141,7 @@ public static long BitPosDriver(byte* input, byte* value, int valLen) /// Find pos of set/clear bit in a sequence of bytes. /// /// Pointer to start of bitmap. - /// + /// The bit value to search for (0 for cleared bit or 1 for set bit). /// Starting offset into bitmap. /// End offset into bitmap. /// @@ -149,27 +149,28 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long { // Mask set to look for 0 or 1 depending on clear/set flag bool bflag = (bSetVal == 0); - long mask = bflag ? -1 : 0; + long mask = bflag ? -1 : 0; // Mask for all 1's (-1 for 0 search) or all 0's (0 for 1 search) long len = (endOffset - startOffset) + 1; - long remainder = len & 7; + long remainder = len & 7; // Check if length is divisible by 8 byte* curr = value + startOffset; - byte* end = curr + (len - remainder); + byte* end = curr + (len - remainder); // Process up to the aligned part of the bitmap - // Search for first word not matching mask. + // Search for first word not matching the mask. while (curr < end) { long v = *(long*)(curr); if (v != mask) break; - curr += 8; + curr += 8; // Move by 64-bit chunks } - long pos = (((long)(curr - value)) << 3); + // Calculate bit position from start of bitmap + long pos = (((long)(curr - value)) << 3); // Convert byte position to bit position long payload = 0; - // Adjust end so we can retrieve word + // Adjust end to account for remainder end = end + remainder; - // Build payload at least one byte to examine + // Build payload from remaining bytes if (curr < end) payload |= (long)curr[0] << 56; if (curr + 1 < end) payload |= (long)curr[1] << 48; if (curr + 2 < end) payload |= (long)curr[2] << 40; @@ -179,14 +180,25 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long if (curr + 6 < end) payload |= (long)curr[6] << 8; if (curr + 7 < end) payload |= (long)curr[7]; - // Transform to count leading zeros - payload = (bSetVal == 0) ? ~payload : payload; + // Transform payload for bit search + payload = (bflag) ? ~payload : payload; + // Handle edge cases where the bitmap is all 0's or all 1's if (payload == mask) - return -1; + { + if (!bflag) + { + return -1; + } + } + // Otherwise, count leading zeros to find the position of the first 1 or 0 pos += (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); + // if we are exceeding it, return -1 + if (pos >= len * 8) + return -1; + return pos; } } diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 70c452d3d1..044830badb 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -529,7 +529,7 @@ static bool TryInPlaceUpdateNumber(ref SpanByte value, ref SpanByteAndMemory out return true; } - return InPlaceUpdateNumber(val, ref value, ref output, ref rmwInfo, ref recordInfo, valueOffset, valueOffset); + return InPlaceUpdateNumber(val, ref value, ref output, ref rmwInfo, ref recordInfo, valueOffset); } static void CopyUpdateNumber(long next, ref SpanByte newValue, ref SpanByteAndMemory output, int etagIgnoredOffset) @@ -620,37 +620,6 @@ static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, re CopyUpdateNumber(val, ref newValue, ref output, etagIgnoredOffset); } - /// - /// Copy update from old value to new value while also validating whether oldValue is a numerical value. - /// - /// Old value copying from - /// New value copying to - /// Output value - /// Parsed input value - static void TryCopyUpdateNumber(ref SpanByte oldValue, ref SpanByte newValue, ref SpanByteAndMemory output, double input) - { - newValue.ExtraMetadata = oldValue.ExtraMetadata; - - // Check if value contains a valid number - if (!IsValidDouble(oldValue.LengthWithoutMetadata, oldValue.ToPointer(), output.SpanByte.AsSpan(), out var val)) - { - // Move to tail of the log even when oldValue is alphanumeric - // We have already paid the cost of bringing from disk so we are treating as a regular access and bring it into memory - oldValue.CopyTo(ref newValue); - return; - } - - val += input; - if (!double.IsFinite(val)) - { - output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; - return; - } - - // Move to tail of the log and update - CopyUpdateNumber(val, ref newValue, ref output); - } - /// /// Parse ASCII byte array into long and validate that only contains ASCII decimal characters /// diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 818b125e71..859b626ead 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -435,7 +435,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref SpanByte input, ref Span // Check if input contains a valid number if (!IsValidDouble(length, inputPtr + RespInputHeader.Size, output.SpanByte.AsSpan(), out var incrByFloat)) return true; - return TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat); + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat, etagIgnoredOffset)) + return false; + break; case RespCommand.SETBIT: byte* i = inputPtr + RespInputHeader.Size; @@ -868,7 +870,7 @@ public bool CopyUpdater(ref SpanByte key, ref SpanByte input, ref SpanByte oldVa oldValue.CopyTo(ref newValue); break; } - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat, etagIgnoredOffset); break; case RespCommand.SETBIT: diff --git a/test/Garnet.test/GarnetBitmapTests.cs b/test/Garnet.test/GarnetBitmapTests.cs index 88ad67c31c..6d9c6cb61f 100644 --- a/test/Garnet.test/GarnetBitmapTests.cs +++ b/test/Garnet.test/GarnetBitmapTests.cs @@ -165,11 +165,6 @@ public void BitmapSimpleSetGet_PCT(int bytesPerSend) public void BitmapSetGetBitTest_LTM(bool preSet) { int bitmapBytes = 512; - server.Dispose(); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, - lowMemory: true, - MemorySize: (bitmapBytes << 2).ToString(), - PageSize: (bitmapBytes << 1).ToString()); server.Start(); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -677,6 +672,28 @@ public void BitmapBitPosTest_LTM() } } + [Test] + [Category("BITPOS")] + public void BitmapBitPosTest_BoundaryConditions() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + const int bitmapSize = 24; + byte[] bitmap = new byte[bitmapSize]; + + string key = "mybitmap"; + ClassicAssert.IsTrue(db.StringSet(key, bitmap)); + + // first unset bit, should increment + for (int i = 0; i < bitmapSize; i ++) + { + // first unset bit + ClassicAssert.AreEqual(i, db.StringBitPosition(key, false)); + ClassicAssert.IsFalse(db.StringSetBit(key, i, true)); + } + } + [Test, Order(15)] [TestCase(10)] [TestCase(20)] diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 17bd45e58a..6eaa82f1d2 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading; @@ -865,6 +866,41 @@ public void SimpleIncrementOverflowForEtagSetData(RespCommand cmd) ClassicAssert.IsTrue(exception); } + [Test] + [TestCase(0, 12.6)] + [TestCase(12.6, 0)] + [TestCase(10, 10)] + [TestCase(910151, 0.23659)] + [TestCase(663.12336412, 12342.3)] + [TestCase(10, -110)] + [TestCase(110, -110.234)] + [TestCase(-2110.95255555, -110.234)] + [TestCase(-2110.95255555, 100000.526654512219412)] + [TestCase(double.MaxValue, double.MinValue)] + public void SimpleIncrementByFloatForEtagSetData(double initialValue, double incrByValue) + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + var key = "key1"; + db.Execute("SETWITHETAG", key, initialValue); + + var expectedResult = initialValue + incrByValue; + + var actualResultStr = (string)db.Execute("INCRBYFLOAT", [key, incrByValue]); + var actualResultRawStr = db.StringGet(key); + + var actualResult = double.Parse(actualResultStr, CultureInfo.InvariantCulture); + var actualResultRaw = double.Parse(actualResultRawStr, CultureInfo.InvariantCulture); + + Assert.That(actualResult, Is.EqualTo(expectedResult).Within(1.0 / Math.Pow(10, 15))); + Assert.That(actualResult, Is.EqualTo(actualResultRaw).Within(1.0 / Math.Pow(10, 15))); + + RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", key); + long etag = (long)res[0]; + double value = double.Parse(res[1].ToString(), CultureInfo.InvariantCulture); + Assert.That(value, Is.EqualTo(actualResultRaw).Within(1.0 / Math.Pow(10, 15))); + ClassicAssert.AreEqual(1, etag); + } [Test] public void SingleDeleteForEtagSetData() @@ -1911,6 +1947,12 @@ public void SetBitOperationsOnEtagSetData() long firstSetBitPosition = db.StringBitPosition(key, true); ClassicAssert.AreEqual(0, firstSetBitPosition); // As we are setting bits in order, first set bit should be 0 + // find the first unset bit + long firstUnsetBitPos = db.StringBitPosition(key, false); + long firstUnsetBitPosExpected = i == 63 ? -1 : i + 1; + ClassicAssert.AreEqual(firstUnsetBitPosExpected, firstUnsetBitPos); // As we are setting bits in order, first unset bit should be 1 ahead + + // with each bit set that we do, we are increasing the etag as well by 1 etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); ClassicAssert.AreEqual(expectedBitCount, etagToCheck); @@ -1932,6 +1974,11 @@ public void SetBitOperationsOnEtagSetData() setbits = db.StringBitCount(key); ClassicAssert.AreEqual(expectedBitCount, setbits); + // find the first set bit + long firstSetBit = db.StringBitPosition(key, true); + long expectedSetBit = i == 0 ? -1 : 0; + ClassicAssert.AreEqual(expectedSetBit, firstSetBit); + // Use BitPosition to find the first unset bit long firstUnsetBitPosition = db.StringBitPosition(key, false); ClassicAssert.AreEqual(i, firstUnsetBitPosition); // After unsetting, the first unset bit should be i From 8f09c7e6e5b5bdcb2ce6254669c76c77d77ff1a5 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sat, 16 Nov 2024 15:10:59 -0800 Subject: [PATCH 27/87] WIP --- libs/server/Resp/BasicCommands.cs | 40 +-- .../Storage/Functions/MainStore/RMWMethods.cs | 308 +++++------------- .../Functions/MainStore/ReadMethods.cs | 8 +- 3 files changed, 83 insertions(+), 273 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index c09cb6311d..a9bd867c28 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -321,16 +321,8 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) Debug.Assert(parseState.Count == 1); var key = parseState.GetArgSliceByRef(0).SpanByte; + var input = new RawStringInput(RespCommand.GETWITHETAG, ref parseState, startIdx: 1); var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - - // Setup input buffer - // (len of spanbyte + input header size) is our input buffer size - var inputSize = sizeof(int) + RespInputHeader.Size; - SpanByte input = SpanByte.Reinterpret(stackalloc byte[inputSize]); - byte* inputPtr = input.ToPointer(); - ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; - ((RespInputHeader*)inputPtr)->flags = 0; - var status = storageApi.GET(ref key, ref input, ref output); switch (status) @@ -362,19 +354,8 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) Debug.Assert(parseState.Count == 2); var key = parseState.GetArgSliceByRef(0).SpanByte; - var etagToCheckWith = parseState.GetLong(1); - + var input = new RawStringInput(RespCommand.GETIFNOTMATCH, ref parseState, startIdx: 1); var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - - // Setup input buffer to pass command info, and the ETag to check with. - // len + header + etag's data type size - var inputSize = RespInputHeader.Size + Constants.EtagSize + sizeof(int); - SpanByte input = SpanByte.Reinterpret(stackalloc byte[inputSize]); - byte* inputPtr = input.ToPointer(); - ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETIFNOTMATCH; - ((RespInputHeader*)inputPtr)->flags = 0; - *(long*)(inputPtr + RespInputHeader.Size) = etagToCheckWith; - var status = storageApi.GET(ref key, ref input, ref output); switch (status) @@ -408,21 +389,9 @@ private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) Debug.Assert(parseState.Count == 3); var key = parseState.GetArgSliceByRef(0).SpanByte; - var value = parseState.GetArgSliceByRef(1).SpanByte; - long etagToCheckWith = parseState.GetLong(2); - - /* - The network buffer holds key, value, and etag in a contiguous chunk of memory, in order, along with padding for separators in RESP. - Shift the etag down over the post-value padding to immediately follow the value: - [] -> [] - */ - - int initialSizeOfValueSpan = value.Length; - value.Length = initialSizeOfValueSpan + Constants.EtagSize; - *(long*)(value.ToPointer() + initialSizeOfValueSpan) = etagToCheckWith; // Here Etag retain argument does not really matter because setifmatch may or may not update etag based on the "if match" condition - NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, ref key, value.ToPointer() - sizeof(int), value.Length, getValue: true, highPrecision: false, retainEtag: true, ref storageApi); + NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, ref key, getValue: true, highPrecision: false, retainEtag: true, ref storageApi); return true; } @@ -438,7 +407,6 @@ private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) Debug.Assert(parseState.Count == 2 || parseState.Count == 3); var key = parseState.GetArgSliceByRef(0).SpanByte; - var value = parseState.GetArgSliceByRef(1).SpanByte; bool retainEtag = false; if (parseState.Count == 3) @@ -467,7 +435,7 @@ private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) } // calling set with etag on an exisitng key will update the etag of the existing key - NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, ref key, value.ToPointer() - sizeof(int), value.Length, getValue: true, highPrecision: false, retainEtag, ref storageApi); + NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, ref key, getValue: true, highPrecision: false, retainEtag, ref storageApi); return true; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 404348192c..2ac0790c16 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -129,7 +129,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; newValue.CopyTo(value.AsSpan().Slice(offset)); - CopyValueLengthToOutput(ref value, ref output, 0); + CopyValueLengthToOutput(ref value, ref output); break; case RespCommand.APPEND: @@ -137,7 +137,8 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB // Copy value to be appended to the newly allocated value buffer appendValue.ReadOnlySpan.CopyTo(value.AsSpan()); - CopyValueLengthToOutput(ref value, ref output, 0); + + CopyValueLengthToOutput(ref value, ref output); break; case RespCommand.INCR: value.UnmarkExtraMetadata(); @@ -150,8 +151,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB var incrBy = input.arg1; var ndigits = NumUtils.NumDigitsInLong(incrBy, ref fNeg); value.ShrinkSerializedLength(ndigits + (fNeg ? 1 : 0)); - // If incrby is being made for initial update then it was not made with etag so the offset is sent as 0 - CopyUpdateNumber(incrBy, ref value, ref output, 0); + CopyUpdateNumber(incrBy, ref value, ref output); break; case RespCommand.DECR: value.UnmarkExtraMetadata(); @@ -164,22 +164,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB var decrBy = -input.arg1; ndigits = NumUtils.NumDigitsInLong(decrBy, ref fNeg); value.ShrinkSerializedLength(ndigits + (fNeg ? 1 : 0)); - // If incrby is being made for initial update then it was not made with etag so the offset is sent as 0 - CopyUpdateNumber(decrBy, ref value, ref output, 0); - break; - case RespCommand.SETWITHETAG: - recordInfo.SetHasETag(); - - // Copy input to value - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + Constants.EtagSize); - value.ExtraMetadata = input.ExtraMetadata; - - // initial etag set to 0, this is a counter based etag that is incremented on change - *(long*)value.ToPointer() = 0; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(Constants.EtagSize)); - - // Copy initial etag to output - CopyRespNumber(0, ref output); + CopyUpdateNumber(decrBy, ref value, ref output); break; case RespCommand.INCRBYFLOAT: value.UnmarkExtraMetadata(); @@ -191,6 +176,16 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB } CopyUpdateNumber(incrByFloat, ref value, ref output); break; + case RespCommand.SETWITHETAG: + recordInfo.SetHasETag(); + var valueToSet = input.parseState.GetArgSliceByRef(0); + value.ShrinkSerializedLength(value.MetadataSize + valueToSet.Length + Constants.EtagSize); + // initial etag set to 0, this is a counter based etag that is incremented on change + *(long*)valueToSet.SpanByte.ToPointer() = 0; + valueToSet.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); + // Copy initial etag to output + CopyRespNumber(0, ref output); + break; default: value.UnmarkExtraMetadata(); recordInfo.ClearHasETag(); @@ -267,11 +262,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return false; } - // First byte of input payload identifies command var cmd = input.header.cmd; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; + int etagIgnoredEnd = 0; long oldEtag = -1; if (recordInfo.ETag) { @@ -280,6 +273,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re oldEtag = *(long*)value.ToPointer(); } + // First byte of input payload identifies command switch (cmd) { case RespCommand.SETEXNX: @@ -290,7 +284,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } return true; - case RespCommand.SETIFMATCH: // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH // In this case no etag will match the "nil" etag on a record without an etag @@ -300,20 +293,19 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return false; } - long prevEtag = *(long*)value.ToPointer(); - - byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; - long etagFromClient = *(long*)locationOfEtagInInputPtr; + var prevEtag = *(long*)value.ToPointer(); + var etagFromClient = input.parseState.GetLong(2); if (prevEtag != etagFromClient) { - // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH + // Cancelling the operation and returning false is used to indicate ETAGMISMATCH rmwInfo.Action = RMWAction.CancelOperation; return false; } - // Need CU if no space for new value - if (input.Length - RespInputHeader.Size > value.Length) + // Need Copy update if no space for new value + var inputValue = input.parseState.GetArgSliceByRef(1); + if (value.Length - Constants.EtagSize > inputValue.length) return false; // Increment the ETag @@ -322,27 +314,21 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size); - - // Copy input to value - value.ExtraMetadata = input.ExtraMetadata; + value.ShrinkSerializedLength(inputValue.Length); *(long*)value.ToPointer() = newEtag; - input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - Constants.EtagSize)[RespInputHeader.Size..].CopyTo(value.AsSpan(Constants.EtagSize)); + inputValue.SpanByte.CopyTo(value.AsSpan(Constants.EtagSize)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1, true); // early return since we already updated the ETag - return true; - + return true; case RespCommand.SET: case RespCommand.SETEXXX: - var setValue = input.parseState.GetArgSliceByRef(0); - var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) + if(input.header.CheckRetainEtagFlag()) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record nextUpdateEtagOffset = 0; @@ -350,21 +336,23 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re recordInfo.ClearHasETag(); } + var setValue = input.parseState.GetArgSliceByRef(0); + // Need CU if no space for new value var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - if (setValue.Length + metadataSize > value.Length - etagIgnoredOffset) return false; + if (setValue.Length + metadataSize > value.Length - etagIgnoredEnd) return false; // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output, nextUpdateEtagOffset,etagIgnoredEnd); } // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(setValue.Length + metadataSize + nextUpdateEtagOffset); + value.ShrinkSerializedLength(setValue.Length + metadataSize + nextUpdateEtagIgnoredEnd); // Copy input to value value.ExtraMetadata = input.arg1; @@ -372,12 +360,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); break; - case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: - setValue = input.parseState.GetArgSliceByRef(0); - // respect etag retention only if input header tells you to explicitly - if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) + if (!input.header.CheckRetainEtagFlag()) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record etagIgnoredOffset = 0; @@ -385,6 +370,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re recordInfo.ClearHasETag(); } + setValue = input.parseState.GetArgSliceByRef(0); // Need CU if no space for new value if (setValue.Length + value.MetadataSize > value.Length - etagIgnoredOffset) return false; @@ -402,6 +388,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy input to value setValue.ReadOnlySpan.CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + // etag is not updated return true; case RespCommand.PEXPIRE: @@ -444,24 +431,25 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; case RespCommand.INCR: - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) + if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) return false; break; + case RespCommand.DECR: - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, etagIgnoredOffset)) + if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, etagIgnoredOffset)) return false; break; case RespCommand.INCRBY: // Check if input contains a valid number var incrBy = input.arg1; - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, etagIgnoredOffset)) + if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, etagIgnoredOffset)) return false; break; case RespCommand.DECRBY: var decrBy = input.arg1; - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, etagIgnoredOffset)) + if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, etagIgnoredOffset)) return false; break; @@ -477,12 +465,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re break; case RespCommand.SETBIT: - var v = value.ToPointer(); + var v = value.ToPointer() + etagIgnoredOffset; var bOffset = input.parseState.GetLong(0); - var bSetVal = (byte)(input.parseState.GetArgSliceByRef(1).ReadOnlySpan[0] - '0') + etagIgnoredOffset; + var bSetVal = (byte)(input.parseState.GetArgSliceByRef(1).ReadOnlySpan[0] - '0'); - // the "- etagIgnoredOffset" accounts for subtracting the space for the etag in the payload if it exists in the Value - if (!BitmapManager.IsLargeEnough(value.Length, bOffset - etagIgnoredOffset)) return false; + if (!BitmapManager.IsLargeEnough(value.Length - etagIgnoredOffset, bOffset)) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); @@ -496,10 +483,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_1, ref output); break; case RespCommand.BITFIELD: + // HK TODO FROM HERE var bitFieldArgs = GetBitFieldArguments(ref input); v = value.ToPointer() + etagIgnoredOffset; - - // the "- etagIgnoredOffset" accounts for subtracting the space for the etag in the payload if it exists in the Value if (!BitmapManager.IsLargeEnoughForType(bitFieldArgs, value.Length - etagIgnoredOffset)) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); @@ -513,8 +499,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyRespNumber(bitfieldReturnValue, ref output); else CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); - - break; + return true; case RespCommand.PFADD: v = value.ToPointer(); @@ -533,8 +518,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (result) *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; - - // doesnt update etag because this doesnt work with etag data return result; case RespCommand.PFMERGE: @@ -551,25 +534,23 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(value.Length + value.MetadataSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - // doesnt update etag because this doesnt work with etag data return HyperLogLog.DefaultHLL.TryMerge(srcHLL, dstHLL, value.Length); - case RespCommand.SETRANGE: var offset = input.parseState.GetInt(0); var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - if (newValue.Length + offset > value.LengthWithoutMetadata - etagIgnoredOffset) + if (newValue.Length + offset > value.LengthWithoutMetadata) return false; - newValue.CopyTo(value.AsSpan(etagIgnoredOffset).Slice(offset)); + newValue.CopyTo(value.AsSpan().Slice(offset)); - CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); - break; + CopyValueLengthToOutput(ref value, ref output); + return true; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output); rmwInfo.Action = RMWAction.ExpireAndStop; return false; @@ -602,26 +583,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } return true; - case RespCommand.SETWITHETAG: - if (input.Length - RespInputHeader.Size + Constants.EtagSize > value.Length) - return false; - - // retain the older etag (and increment it to account for this update) if requested and if it also exists otherwise set etag to initial etag of 0 - long etagVal = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag() && recordInfo.ETag ? (oldEtag + 1) : 0; - - recordInfo.SetHasETag(); - - // Copy input to value - value.ShrinkSerializedLength(input.Length - RespInputHeader.Size + Constants.EtagSize); - value.ExtraMetadata = input.ExtraMetadata; - - *(long*)value.ToPointer() = etagVal; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(value.AsSpan(Constants.EtagSize)); - - // Copy initial etag to output - CopyRespNumber(etagVal, ref output); - // early return since initial etag setting does not need to be incremented - return true; case RespCommand.APPEND: // If nothing to append, can avoid copy update. @@ -629,7 +590,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (appendSize == 0) { - CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref value, ref output); return true; } @@ -663,13 +624,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.ExtraMetadata = expiration; } - var valueLength = value.LengthWithoutMetadata - etagIgnoredOffset; + var valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); - var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), ref input, value.AsSpan(etagIgnoredOffset), ref valueLength, ref outp, ref rmwInfo); + var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), ref input, value.AsSpan(), ref valueLength, ref outp, ref rmwInfo); Debug.Assert(valueLength <= value.LengthWithoutMetadata); // Adjust value length if user shrinks it - if (valueLength < value.LengthWithoutMetadata - etagIgnoredOffset) + if (valueLength < value.LengthWithoutMetadata) { rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(valueLength + value.MetadataSize); @@ -678,52 +639,17 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re output.Memory = outp.Memory; output.Length = outp.Length; - if (!ret) - return false; - - break; + return ret; } throw new GarnetException("Unsupported operation on input"); } - - // increment the Etag transparently if in place update happened - if (recordInfo.ETag && rmwInfo.Action == RMWAction.Default) - { - *(long*)value.ToPointer() = oldEtag + 1; - } - return true; } /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { - - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - if (rmwInfo.RecordInfo.ETag) - { - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - } - switch (input.header.cmd) { - case RespCommand.SETIFMATCH: - if (!rmwInfo.RecordInfo.ETag) - return false; - - long existingEtag = *(long*)oldValue.ToPointer(); - - byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; - long etagToCheckWith = *(long*)locationOfEtagInInputPtr; - - if (existingEtag != etagToCheckWith) - { - // cancellation and return false indicates ETag mismatch - rmwInfo.Action = RMWAction.CancelOperation; - return false; - } - return true; case RespCommand.SETEXNX: // Expired data, return false immediately // ExpireAndResume ensures that we set as new value, since it does not exist @@ -736,7 +662,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output); } return false; case RespCommand.SETEXXX: @@ -753,7 +679,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.customCommands[(ushort)input.header.cmd - CustomCommandManager.StartOffset].functions - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -775,77 +701,15 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - var cmd = (RespCommand)(*inputPtr); - - bool shouldUpdateEtag = true; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = -1; - if (recordInfo.ETag) - { - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - oldEtag = *(long*)oldValue.ToPointer(); - } - - switch (cmd) + switch (input.header.cmd) { - case RespCommand.SETWITHETAG: - Debug.Assert(input.Length - RespInputHeader.Size + Constants.EtagSize == newValue.Length); - - // etag setting will be done here so does not need to be incremented outside switch - shouldUpdateEtag = false; - // retain the older etag (and increment it to account for this update) if requested and if it also exists otherwise set etag to initial etag of 0 - long etagVal = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag() && recordInfo.ETag ? (oldEtag + 1) : 0; - recordInfo.SetHasETag(); - // Copy input to value - newValue.ExtraMetadata = input.ExtraMetadata; - input.AsReadOnlySpan()[RespInputHeader.Size..].CopyTo(newValue.AsSpan(Constants.EtagSize)); - // set the etag - *(long*)newValue.ToPointer() = etagVal; - // Copy initial etag to output - CopyRespNumber(etagVal, ref output); - break; - - case RespCommand.SETIFMATCH: - Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data. Inplace update should have returned mismatch."); - - // this update is so the early call to send the resp command works, outside of the switch - // we are doing a double op of setting the etag to normalize etag update for other operations - byte* locationOfEtagInInputPtr = inputPtr + input.LengthWithoutMetadata - Constants.EtagSize; - long etagToCheckWith = *(long*)locationOfEtagInInputPtr; - - // Copy input to value - newValue.ExtraMetadata = input.ExtraMetadata; - - *(long*)newValue.ToPointer() = etagToCheckWith + 1; - input.AsReadOnlySpan().Slice(0, input.LengthWithoutMetadata - Constants.EtagSize)[RespInputHeader.Size..].CopyTo(newValue.AsSpan(Constants.EtagSize)); - - - // Write Etag and Val back to Client - CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1, true); - break; - case RespCommand.SET: case RespCommand.SETEXXX: - var nextUpdateEtagOffset = etagIgnoredOffset; - var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) - { - // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record - nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - recordInfo.ClearHasETag(); - } - - // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX - Debug.Assert(input.Length - RespInputHeader.Size == newValue.Length - etagIgnoredOffset); - // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output); } // Copy input to value @@ -861,32 +725,22 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - nextUpdateEtagOffset = etagIgnoredOffset; - nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!((RespInputHeader*)inputPtr)->CheckRetainEtagFlag()) - { - // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record - nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - } - Debug.Assert(oldValue.MetadataSize + setValue.Length == newValue.Length); // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output); } // Copy input to value, retain metadata of oldValue newValue.ExtraMetadata = oldValue.ExtraMetadata; - setValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); + setValue.CopyTo(newValue.AsSpan()); break; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: - shouldUpdateEtag = false; var expiryExists = oldValue.MetadataSize > 0; var expiryValue = input.parseState.GetLong(0); @@ -901,7 +755,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.PEXPIREAT: case RespCommand.EXPIREAT: - shouldUpdateEtag = false; expiryExists = oldValue.MetadataSize > 0; var expiryTimestamp = input.parseState.GetLong(0); @@ -914,7 +767,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.PERSIST: - shouldUpdateEtag = false; oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); if (oldValue.MetadataSize != 0) { @@ -926,21 +778,21 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.INCR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1); break; case RespCommand.DECR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1); break; case RespCommand.INCRBY: var incrBy = input.arg1; - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy); break; case RespCommand.DECRBY: var decrBy = input.arg1; - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy); break; case RespCommand.INCRBYFLOAT: @@ -951,13 +803,13 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue.CopyTo(ref newValue); break; } - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat); break; case RespCommand.SETBIT: var bOffset = input.parseState.GetLong(0); var bSetVal = (byte)(input.parseState.GetArgSliceByRef(1).ReadOnlySpan[0] - '0'); - Buffer.MemoryCopy(oldValue.ToPointer() + etagIgnoredOffset, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); + Buffer.MemoryCopy(oldValue.ToPointer(), newValue.ToPointer(), newValue.Length, oldValue.Length); var oldValSet = BitmapManager.UpdateBitmap(newValue.ToPointer(), bOffset, bSetVal); if (oldValSet == 0) CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_0, ref output); @@ -967,8 +819,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); - Buffer.MemoryCopy(oldValue.ToPointer() + etagIgnoredOffset, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); - var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset); + Buffer.MemoryCopy(oldValue.ToPointer(), newValue.ToPointer(), newValue.Length, oldValue.Length); + var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer(), newValue.Length); if (!overflow) CopyRespNumber(bitfieldReturnValue, ref output); @@ -977,7 +829,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.PFADD: - // HYPERLOG doesnt work with non hyperlog key values var updated = false; var newValPtr = newValue.ToPointer(); var oldValPtr = oldValue.ToPointer(); @@ -986,14 +837,13 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte updated = HyperLogLog.DefaultHLL.CopyUpdate(ref input, oldValPtr, newValPtr, newValue.Length); else { - Buffer.MemoryCopy(oldValPtr, newValPtr, newValue.Length - etagIgnoredOffset, oldValue.Length); + Buffer.MemoryCopy(oldValPtr, newValPtr, newValue.Length, oldValue.Length); HyperLogLog.DefaultHLL.Update(ref input, newValPtr, newValue.Length, ref updated); } *output.SpanByte.ToPointer() = updated ? (byte)1 : (byte)0; break; case RespCommand.PFMERGE: - // HYPERLOG doesnt work with non hyperlog key values //srcA offset: [hll allocated size = 4 byte] + [hll data structure] //memcpy +4 (skip len size) var srcHLLPtr = input.parseState.GetArgSliceByRef(0).SpanByte.ToPointer(); // HLL merging from var oldDstHLLPtr = oldValue.ToPointer(); // original HLL merging to (too small to hold its data plus srcA) @@ -1007,15 +857,15 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue.CopyTo(ref newValue); newInputValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - newInputValue.CopyTo(newValue.AsSpan(etagIgnoredOffset).Slice(offset)); + newInputValue.CopyTo(newValue.AsSpan().Slice(offset)); - CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref newValue, ref output); break; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output); rmwInfo.Action = RMWAction.ExpireAndStop; return false; @@ -1054,10 +904,9 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte var appendValue = input.parseState.GetArgSliceByRef(0); // Append the new value with the client input at the end of the old data - // the oldValue.LengthWithoutMetadata already contains the etag offset here appendValue.ReadOnlySpan.CopyTo(newValue.AsSpan().Slice(oldValue.LengthWithoutMetadata)); - CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref newValue, ref output); break; default: @@ -1079,7 +928,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functions - .CopyUpdater(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), newValue.AsSpan(etagIgnoredOffset), ref outp, ref rmwInfo); + .CopyUpdater(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(), newValue.AsSpan(), ref outp, ref rmwInfo); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -1088,13 +937,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte } rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - - // increment the Etag transparently if in place update happened - if (recordInfo.ETag && shouldUpdateEtag) - { - *(long*)newValue.ToPointer() = oldEtag + 1; - } - return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 45c24204d0..acb9d65015 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -24,8 +24,8 @@ public bool SingleReader(ref SpanByte key, ref RawStringInput input, ref SpanByt if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { - long existingEtag = *(long*)value.ToPointer(); - long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + var existingEtag = *(long*)value.ToPointer(); + var etagToMatchAgainst = input.parseState.GetLong(2); if (existingEtag == etagToMatchAgainst) { // write the value not changed message to dst, and early return @@ -78,8 +78,8 @@ public bool ConcurrentReader(ref SpanByte key, ref RawStringInput input, ref Spa if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { - long existingEtag = *(long*)value.ToPointer(); - long etagToMatchAgainst = *(long*)(input.ToPointer() + RespInputHeader.Size); + var existingEtag = *(long*)value.ToPointer(); + var etagToMatchAgainst = input.parseState.GetLong(2); if (existingEtag == etagToMatchAgainst) { // write the value not changed message to dst, and early return From 8861f758789581723bb94a3b1d4112609f5a41ac Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sun, 15 Dec 2024 22:31:56 -0800 Subject: [PATCH 28/87] unit tests working --- libs/server/Resp/BasicCommands.cs | 31 +- .../server/Resp/Bitmap/BitmapManagerBitPos.cs | 29 +- .../Functions/MainStore/PrivateMethods.cs | 24 +- .../Storage/Functions/MainStore/RMWMethods.cs | 276 +++++++++++++---- .../Functions/MainStore/ReadMethods.cs | 2 +- .../Functions/MainStore/VarLenInputMethods.cs | 23 +- .../Session/MainStore/HyperLogLogOps.cs | 2 +- .../Storage/Session/MainStore/MainStoreOps.cs | 293 ++++++++---------- libs/server/Transaction/TransactionManager.cs | 2 +- test/Garnet.test/RespEtagTests.cs | 8 +- 10 files changed, 393 insertions(+), 297 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 63aba3f54d..a40bbd0922 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -747,22 +747,22 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) if (isEtagRetained) { // cannot do blind upsert if isEtagRetained - return NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, getValue, - highPrecision: false, retainEtag: true, ref storageApi); + return NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, + highPrecision: false, retainEtag: true, storageApi: ref storageApi); } else { return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, getValue: true, + ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue: true, highPrecision: false, retainEtag: false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, highPrecision: false, + : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update } case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, getValue, highPrecision: false, isEtagRetained, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, getValue, highPrecision: false, isEtagRetained, ref storageApi); } @@ -774,22 +774,21 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) if (isEtagRetained) { // cannot do a blind update - return NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, getValue, + return NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, highPrecision: true, retainEtag: true, ref storageApi); } else { return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, getValue: true, + ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue: true, highPrecision: true, retainEtag: false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expiry, ref sbKey, valPtr, vSize, highPrecision: true, - ref storageApi); // Can perform a blind update + : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update } case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, getValue, highPrecision: true, isEtagRetained, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, getValue, highPrecision: true, isEtagRetained, ref storageApi); } @@ -801,13 +800,13 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { case ExistOptions.None: // We can never perform a blind update due to KEEPTTL - return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, ref sbKey, valPtr, vSize, - getValue, highPrecision: false, isEtagRetained, ref storageApi); + return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, ref sbKey + , getValue, highPrecision: false, isEtagRetained, ref storageApi); case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, ref sbKey, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, ref sbKey, getValue, highPrecision: false, isEtagRetained, ref storageApi); case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, valPtr, vSize, + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, getValue, highPrecision: false, isEtagRetained, ref storageApi); } diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index c02bc0feda..950f098f9d 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -138,28 +138,27 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long { // Mask set to look for 0 or 1 depending on clear/set flag bool bflag = (bSetVal == 0); - long mask = bflag ? -1 : 0; // Mask for all 1's (-1 for 0 search) or all 0's (0 for 1 search) + long mask = bflag ? -1 : 0; long len = (endOffset - startOffset) + 1; - long remainder = len & 7; // Check if length is divisible by 8 + long remainder = len & 7; byte* curr = value + startOffset; - byte* end = curr + (len - remainder); // Process up to the aligned part of the bitmap + byte* end = curr + (len - remainder); - // Search for first word not matching the mask. + // Search for first word not matching mask. while (curr < end) { long v = *(long*)(curr); if (v != mask) break; - curr += 8; // Move by 64-bit chunks + curr += 8; } - // Calculate bit position from start of bitmap - long pos = (((long)(curr - value)) << 3); // Convert byte position to bit position + long pos = (((long)(curr - value)) << 3); long payload = 0; - // Adjust end to account for remainder + // Adjust end so we can retrieve word end = end + remainder; - // Build payload from remaining bytes + // Build payload at least one byte to examine if (curr < end) payload |= (long)curr[0] << 56; if (curr + 1 < end) payload |= (long)curr[1] << 48; if (curr + 2 < end) payload |= (long)curr[2] << 40; @@ -169,20 +168,18 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long if (curr + 6 < end) payload |= (long)curr[6] << 8; if (curr + 7 < end) payload |= (long)curr[7]; - // Transform payload for bit search - payload = (bflag) ? ~payload : payload; - - // Handle edge cases where the bitmap is all 0's or all 1's - if (payload == mask) - return pos + 0; + // Transform to count leading zeros + payload = (bSetVal == 0) ? ~payload : payload; - // Otherwise, count leading zeros to find the position of the first 1 or 0 pos += (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); // if we are exceeding it, return -1 if (pos >= len * 8) return -1; + if (payload == mask) + return pos + 0; + return pos; } } diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index e95c0b1b1e..353bf7bf09 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -245,33 +245,23 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB CopyRespTo(ref value, ref dst, start + payloadEtagAccountedEndOffset, end + payloadEtagAccountedEndOffset); return; case RespCommand.SETIFMATCH: - // extract ETAG, write as long into dst, and then value - long etag = *(long*)value.ToPointer(); - // remove the length of the ETAG - int valueLength = value.LengthWithoutMetadata - sizeof(long); - // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below - ReadOnlySpan etagTruncatedVal = value.AsReadOnlySpan(sizeof(long)); - // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n - int desiredLength = 4 + 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; - WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); - return; - case RespCommand.GETIFNOTMATCH: case RespCommand.GETWITHETAG: // If this has an etag then we want to use it other wise null // we know somethgin doesnt have an etag if - etag = -1; - valueLength = value.LengthWithoutMetadata; - + long etag = -1; + int valueLength = value.LengthWithoutMetadata; + int desiredLength; + ReadOnlySpan etagTruncatedVal; if (hasEtagInVal) { // Get value without RESP header; exclude expiration // extract ETAG, write as long into dst, and then value etag = *(long*)value.ToPointer(); // remove the length of the ETAG - valueLength -= sizeof(long); + valueLength -= Constants.EtagSize; // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below - etagTruncatedVal = value.AsReadOnlySpan(sizeof(long)); + etagTruncatedVal = value.AsReadOnlySpan(Constants.EtagSize); // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n desiredLength = 4 + 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; } @@ -337,7 +327,7 @@ static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref b RespWriteUtils.WriteBulkString(value, ref curr, end); } - bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, ref SpanByte input, ref SpanByte value, ref SpanByteAndMemory output) + bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, long newExpiry, ref SpanByte value, ref SpanByteAndMemory output) { ObjectOutputHeader* o = (ObjectOutputHeader*)output.SpanByte.ToPointer(); if (expiryExists) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2ac0790c16..62559ff4ea 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -48,6 +48,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp /// public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo) { + recordInfo.ClearHasETag(); rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); switch (input.header.cmd) @@ -129,7 +130,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; newValue.CopyTo(value.AsSpan().Slice(offset)); - CopyValueLengthToOutput(ref value, ref output); + CopyValueLengthToOutput(ref value, ref output, 0); break; case RespCommand.APPEND: @@ -138,7 +139,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB // Copy value to be appended to the newly allocated value buffer appendValue.ReadOnlySpan.CopyTo(value.AsSpan()); - CopyValueLengthToOutput(ref value, ref output); + CopyValueLengthToOutput(ref value, ref output, 0); break; case RespCommand.INCR: value.UnmarkExtraMetadata(); @@ -177,18 +178,20 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB CopyUpdateNumber(incrByFloat, ref value, ref output); break; case RespCommand.SETWITHETAG: + metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + value.UnmarkExtraMetadata(); + value.ExtraMetadata = input.arg1; recordInfo.SetHasETag(); var valueToSet = input.parseState.GetArgSliceByRef(0); value.ShrinkSerializedLength(value.MetadataSize + valueToSet.Length + Constants.EtagSize); // initial etag set to 0, this is a counter based etag that is incremented on change - *(long*)valueToSet.SpanByte.ToPointer() = 0; + *(long*)value.ToPointer() = 0; valueToSet.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); // Copy initial etag to output CopyRespNumber(0, ref output); break; default: value.UnmarkExtraMetadata(); - recordInfo.ClearHasETag(); if ((ushort)input.header.cmd >= CustomCommandManager.StartOffset) { var functions = functionsState.customCommands[(ushort)input.header.cmd - CustomCommandManager.StartOffset].functions; @@ -264,7 +267,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var cmd = input.header.cmd; int etagIgnoredOffset = 0; - int etagIgnoredEnd = 0; + int etagIgnoredEnd = -1; long oldEtag = -1; if (recordInfo.ETag) { @@ -283,6 +286,32 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } + return true; + case RespCommand.SETWITHETAG: + // SETWITHETAG WILL OVERRIDE the existing value, unless sent with RETAIN ETAG and already has etag + var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + + long etag = recordInfo.ETag && input.header.CheckRetainEtagFlag() ? oldEtag + 1 : 0; + var valueToSet = input.parseState.GetArgSliceByRef(0); + + if (value.Length < valueToSet.length + metadataSize + Constants.EtagSize) + return false; + + recordInfo.SetHasETag(); + + // Adjust value length that will result from this change + rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); + value.ShrinkSerializedLength(metadataSize + valueToSet.Length + Constants.EtagSize); + rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + + if (metadataSize != 0) + value.ExtraMetadata = input.arg1; + *(long*)value.ToPointer() = etag; + valueToSet.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); + + // Copy initial etag to output + CopyRespNumber(etag, ref output); + return true; case RespCommand.SETIFMATCH: // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH @@ -294,7 +323,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } var prevEtag = *(long*)value.ToPointer(); - var etagFromClient = input.parseState.GetLong(2); + var etagFromClient = input.parseState.GetLong(1); if (prevEtag != etagFromClient) { @@ -304,31 +333,31 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // Need Copy update if no space for new value - var inputValue = input.parseState.GetArgSliceByRef(1); - if (value.Length - Constants.EtagSize > inputValue.length) + var inputValue = input.parseState.GetArgSliceByRef(0); + if (value.Length - Constants.EtagSize < inputValue.length) return false; // Increment the ETag long newEtag = prevEtag + 1; - // Adjust value length + // Adjust value length if user shrinks it, how to get rid of spanbyte infront + value.ShrinkSerializedLength(inputValue.Length + Constants.EtagSize); + rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(inputValue.Length); *(long*)value.ToPointer() = newEtag; - inputValue.SpanByte.CopyTo(value.AsSpan(Constants.EtagSize)); - - rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + inputValue.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1, true); + // early return since we already updated the ETag - return true; + return true; case RespCommand.SET: case RespCommand.SETEXXX: + // If user wants to retain etag and the data has etag, we need to silently update/keep the etag, but the response should not be written with the etag var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if(input.header.CheckRetainEtagFlag()) + if (!input.header.CheckRetainEtagFlag()) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record nextUpdateEtagOffset = 0; @@ -339,20 +368,21 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var setValue = input.parseState.GetArgSliceByRef(0); // Need CU if no space for new value - var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - if (setValue.Length + metadataSize > value.Length - etagIgnoredEnd) return false; + metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + if (setValue.Length + metadataSize > value.Length - etagIgnoredOffset) + return false; // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, nextUpdateEtagOffset,etagIgnoredEnd); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(setValue.Length + metadataSize + nextUpdateEtagIgnoredEnd); + value.ShrinkSerializedLength(setValue.Length + metadataSize + nextUpdateEtagOffset); // Copy input to value value.ExtraMetadata = input.arg1; @@ -362,9 +392,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: + // respect etag retention only if input header tells you to explicitly if (!input.header.CheckRetainEtagFlag()) { - // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record etagIgnoredOffset = 0; etagIgnoredEnd = -1; recordInfo.ClearHasETag(); @@ -372,7 +402,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue = input.parseState.GetArgSliceByRef(0); // Need CU if no space for new value - if (setValue.Length + value.MetadataSize > value.Length - etagIgnoredOffset) return false; + if (setValue.Length + value.MetadataSize > value.Length - etagIgnoredOffset) + return false; // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) @@ -403,8 +434,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var expiryTicks = DateTimeOffset.UtcNow.Ticks + tsExpiry.Ticks; var expireOption = (ExpireOption)input.arg1; - return EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output); - + if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) + return false; + return true; ; case RespCommand.PEXPIREAT: case RespCommand.EXPIREAT: expiryExists = value.MetadataSize > 0; @@ -415,7 +447,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re : ConvertUtils.UnixTimestampInSecondsToTicks(expiryTimestamp); expireOption = (ExpireOption)input.arg1; - return EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output); + if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) + return false; + return true; case RespCommand.PERSIST: if (value.MetadataSize != 0) @@ -431,25 +465,25 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; case RespCommand.INCR: - if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) return false; break; case RespCommand.DECR: - if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, etagIgnoredOffset)) return false; break; case RespCommand.INCRBY: // Check if input contains a valid number var incrBy = input.arg1; - if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, etagIgnoredOffset)) return false; break; case RespCommand.DECRBY: var decrBy = input.arg1; - if(!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, etagIgnoredOffset)) return false; break; @@ -483,24 +517,24 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_1, ref output); break; case RespCommand.BITFIELD: - // HK TODO FROM HERE var bitFieldArgs = GetBitFieldArguments(ref input); v = value.ToPointer() + etagIgnoredOffset; - if (!BitmapManager.IsLargeEnoughForType(bitFieldArgs, value.Length - etagIgnoredOffset)) return false; + + if (!BitmapManager.IsLargeEnoughForType(bitFieldArgs, value.Length - etagIgnoredOffset)) + return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); value.ShrinkSerializedLength(value.Length + value.MetadataSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, v, value.Length); + var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, v, value.Length - etagIgnoredOffset); if (!overflow) CopyRespNumber(bitfieldReturnValue, ref output); else CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); - return true; - + break; case RespCommand.PFADD: v = value.ToPointer(); @@ -539,23 +573,23 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var offset = input.parseState.GetInt(0); var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - if (newValue.Length + offset > value.LengthWithoutMetadata) + if (newValue.Length + offset > value.LengthWithoutMetadata - etagIgnoredOffset) return false; - newValue.CopyTo(value.AsSpan().Slice(offset)); + newValue.CopyTo(value.AsSpan(etagIgnoredOffset).Slice(offset)); - CopyValueLengthToOutput(ref value, ref output); - return true; + CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); + break; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref value, ref output); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); rmwInfo.Action = RMWAction.ExpireAndStop; return false; case RespCommand.GETEX: - CopyRespTo(ref value, ref output); + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); if (input.arg1 > 0) { @@ -590,17 +624,15 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (appendSize == 0) { - CopyValueLengthToOutput(ref value, ref output); + CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); return true; } return false; - default: - var cmd = (ushort)input.header.cmd; - if (cmd >= CustomCommandManager.StartOffset) + if ((ushort)cmd >= CustomCommandManager.StartOffset) { - var functions = functionsState.customCommands[cmd - CustomCommandManager.StartOffset].functions; + var functions = functionsState.customCommands[(ushort)cmd - CustomCommandManager.StartOffset].functions; var expiration = input.arg1; if (expiration == -1) { @@ -643,13 +675,42 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } throw new GarnetException("Unsupported operation on input"); } + + // increment the Etag transparently if in place update happened + if (recordInfo.ETag && rmwInfo.Action == RMWAction.Default) + { + *(long*)value.ToPointer() = oldEtag + 1; + } + + return true; } /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + if (rmwInfo.RecordInfo.ETag) + { + etagIgnoredOffset = sizeof(long); + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + } + switch (input.header.cmd) { + case RespCommand.SETIFMATCH: + if (!rmwInfo.RecordInfo.ETag) + return false; + + var etagToCheckWith = input.parseState.GetLong(1); + long existingEtag = *(long*)oldValue.ToPointer(); + if (existingEtag != etagToCheckWith) + { + // cancellation and return false indicates ETag mismatch + rmwInfo.Action = RMWAction.CancelOperation; + return false; + } + return true; case RespCommand.SETEXNX: // Expired data, return false immediately // ExpireAndResume ensures that we set as new value, since it does not exist @@ -662,7 +723,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } return false; case RespCommand.SETEXXX: @@ -679,7 +740,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.customCommands[(ushort)input.header.cmd - CustomCommandManager.StartOffset].functions - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -701,46 +762,116 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - switch (input.header.cmd) + var cmd = input.header.cmd; + var shouldUpdateEtag = true; + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = -1; + if (recordInfo.ETag) + { + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + etagIgnoredOffset = Constants.EtagSize; + oldEtag = *(long*)oldValue.ToPointer(); + } + + switch (cmd) { + case RespCommand.SETWITHETAG: + recordInfo.SetHasETag(); + var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + if (metadataSize != 0) + newValue.ExtraMetadata = input.arg1; + + long etag = input.header.CheckRetainEtagFlag() && recordInfo.ETag ? oldEtag + 1 : 0; + var dest = newValue.AsSpan(Constants.EtagSize); + var src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; + + Debug.Assert(src.Length + Constants.EtagSize == newValue.Length); + + src.CopyTo(dest); + + CopyRespNumber(etag, ref output); + break; + + case RespCommand.SETIFMATCH: + Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data."); + + // avoids double update at the end + shouldUpdateEtag = false; + *(long*)newValue.ToPointer() = oldEtag + 1; + + // Copy input to value + dest = newValue.AsSpan(Constants.EtagSize); + src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; + + Debug.Assert(src.Length + Constants.EtagSize == newValue.Length); + + src.CopyTo(dest); + + // Write Etag and Val back to Client + CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1, hasEtagInVal: true); + break; + case RespCommand.SET: case RespCommand.SETEXXX: + var nextUpdateEtagOffset = etagIgnoredOffset; + var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + if (!input.header.CheckRetainEtagFlag()) + { + // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + recordInfo.ClearHasETag(); + } + // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - Debug.Assert(newInputValue.Length + metadataSize == newValue.Length); + // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX + Debug.Assert(newInputValue.Length + metadataSize + etagIgnoredOffset == newValue.Length); newValue.ExtraMetadata = input.arg1; - newInputValue.CopyTo(newValue.AsSpan()); + newInputValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: + nextUpdateEtagOffset = etagIgnoredOffset; + nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + if (input.header.CheckRetainEtagFlag()) + { + // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + } + var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - Debug.Assert(oldValue.MetadataSize + setValue.Length == newValue.Length); + Debug.Assert(oldValue.MetadataSize + setValue.Length == newValue.Length - etagIgnoredOffset); // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Copy input to value, retain metadata of oldValue newValue.ExtraMetadata = oldValue.ExtraMetadata; - setValue.CopyTo(newValue.AsSpan()); + setValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); break; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: + shouldUpdateEtag = false; + var expiryExists = oldValue.MetadataSize > 0; var expiryValue = input.parseState.GetLong(0); @@ -756,6 +887,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.PEXPIREAT: case RespCommand.EXPIREAT: expiryExists = oldValue.MetadataSize > 0; + shouldUpdateEtag = false; var expiryTimestamp = input.parseState.GetLong(0); expiryTicks = input.header.cmd == RespCommand.PEXPIREAT @@ -767,6 +899,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.PERSIST: + shouldUpdateEtag = false; oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); if (oldValue.MetadataSize != 0) { @@ -778,21 +911,21 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.INCR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1, etagIgnoredOffset); break; case RespCommand.DECR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1, etagIgnoredOffset); break; case RespCommand.INCRBY: var incrBy = input.arg1; - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy, etagIgnoredOffset); break; case RespCommand.DECRBY: var decrBy = input.arg1; - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy, etagIgnoredOffset); break; case RespCommand.INCRBYFLOAT: @@ -803,7 +936,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue.CopyTo(ref newValue); break; } - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat, etagIgnoredOffset); break; case RespCommand.SETBIT: @@ -819,8 +952,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); - Buffer.MemoryCopy(oldValue.ToPointer(), newValue.ToPointer(), newValue.Length, oldValue.Length); - var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer(), newValue.Length); + Buffer.MemoryCopy(oldValue.ToPointer() + etagIgnoredOffset, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); + var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset); if (!overflow) CopyRespNumber(bitfieldReturnValue, ref output); @@ -857,20 +990,20 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue.CopyTo(ref newValue); newInputValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - newInputValue.CopyTo(newValue.AsSpan().Slice(offset)); + newInputValue.CopyTo(newValue.AsSpan(etagIgnoredOffset).Slice(offset)); - CopyValueLengthToOutput(ref newValue, ref output); + CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); break; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); rmwInfo.Action = RMWAction.ExpireAndStop; return false; case RespCommand.GETEX: - CopyRespTo(ref oldValue, ref output); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); if (input.arg1 > 0) { @@ -906,7 +1039,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Append the new value with the client input at the end of the old data appendValue.ReadOnlySpan.CopyTo(newValue.AsSpan().Slice(oldValue.LengthWithoutMetadata)); - CopyValueLengthToOutput(ref newValue, ref output); + CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); break; default: @@ -928,7 +1061,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functions - .CopyUpdater(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(), newValue.AsSpan(), ref outp, ref rmwInfo); + .CopyUpdater(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), newValue.AsSpan(etagIgnoredOffset), ref outp, ref rmwInfo); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -937,6 +1070,13 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte } rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); + + // increment the Etag transparently if in place update happened + if (recordInfo.ETag && shouldUpdateEtag) + { + *(long*)newValue.ToPointer() = oldEtag + 1; + } + return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index acb9d65015..1550c72a74 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -79,7 +79,7 @@ public bool ConcurrentReader(ref SpanByte key, ref RawStringInput input, ref Spa if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) { var existingEtag = *(long*)value.ToPointer(); - var etagToMatchAgainst = input.parseState.GetLong(2); + var etagToMatchAgainst = input.parseState.GetLong(0); if (existingEtag == etagToMatchAgainst) { // write the value not changed message to dst, and early return diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 170ff078a0..44855c08bf 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System.Diagnostics; using Garnet.common; using Tsavorite.core; @@ -69,7 +68,9 @@ static bool IsValidDouble(int length, byte* source, out double val) /// public int GetRMWInitialValueLength(ref RawStringInput input) { + var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); var cmd = input.header.cmd; + switch (cmd) { case RespCommand.SETBIT: @@ -111,7 +112,8 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + ndigits + (fNeg ? 1 : 0); case RespCommand.SETWITHETAG: // same space as SET but with 8 additional bytes for etag at the front of the payload - return sizeof(int) + input.Length - RespInputHeader.Size + Constants.EtagSize; + newValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; + return sizeof(int) + newValue.Length + Constants.EtagSize + metadataSize; case RespCommand.INCRBYFLOAT: if (!input.parseState.TryGetDouble(0, out var incrByFloat)) return sizeof(int); @@ -125,7 +127,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) { var functions = functionsState.customCommands[(ushort)cmd - CustomCommandManager.StartOffset].functions; // Compute metadata size for result - int metadataSize = input.arg1 switch + metadataSize = input.arg1 switch { -1 => 0, 0 => 0, @@ -134,8 +136,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + - (input.arg1 == 0 ? 0 : sizeof(long)); + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize; } } @@ -146,7 +147,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b { var cmd = input.header.cmd; int etagOffset = hasEtag ? Constants.EtagSize : 0; - bool retainEtag = ((RespInputHeader*)inputPtr)->CheckRetainEtagFlag(); + bool retainEtag = input.header.CheckRetainEtagFlag(); switch (cmd) { @@ -215,13 +216,13 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b case RespCommand.SETEXXX: if (!retainEtag) etagOffset = 0; - return sizeof(int) + input.Length - RespInputHeader.Size + etagOffset; - case RespCommand.SETIFMATCH: + return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + etagOffset; case RespCommand.PERSIST: return sizeof(int) + t.LengthWithoutMetadata; case RespCommand.SETWITHETAG: - // same space as SET but with 8 additional bytes for etag at the front of the payload - return sizeof(int) + input.Length - RespInputHeader.Size + Constants.EtagSize; + case RespCommand.SETIFMATCH: + var newValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; + return sizeof(int) + newValue.Length + Constants.EtagSize + t.MetadataSize + (input.arg1 == 0 ? 0 : sizeof(long)); case RespCommand.EXPIRE: case RespCommand.PEXPIRE: case RespCommand.EXPIREAT: @@ -230,7 +231,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b case RespCommand.SETRANGE: var offset = input.parseState.GetInt(0); - var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; + newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; if (newValue.Length + offset > t.LengthWithoutMetadata - etagOffset) return sizeof(int) + newValue.Length + offset + t.MetadataSize + etagOffset; diff --git a/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs b/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs index f09d34e7c9..ac0f877754 100644 --- a/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs +++ b/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs @@ -244,7 +244,7 @@ public unsafe GarnetStatus HyperLogLogMerge(ref RawStringInput input, out bool e parseState.InitializeWithArgument(mergeSlice); currInput.parseState = parseState; - SET_Conditional(ref dstKey, ref currInput, ref mergeBuffer, ref currLockableContext); + SET_Conditional(ref dstKey, ref currInput, ref mergeBuffer, ref currLockableContext, input.header.cmd); #endregion } diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 1d9b6f8f77..57fc9bf830 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -376,37 +376,8 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawSt } } - public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, ref TContext context) - where TContext : ITsavoriteContext - { - var status = context.RMW(ref key, ref input, ref output); - - if (status.IsPending) - { - StartPendingMetrics(); - CompletePendingForSession(ref status, ref output, ref context); - StopPendingMetrics(); - } - - if (status.NotFound) - { - incr_session_notfound(); - return GarnetStatus.NOTFOUND; - } - else if (cmd == RespCommand.SETIFMATCH && status.IsCanceled) - { - // The RMW operation for SETIFMATCH upon not finding the etags match between the existing record and sent etag returns Cancelled Operation - return GarnetStatus.ETAGMISMATCH; - } - else - { - incr_session_found(); - return GarnetStatus.OK; - } - } - - public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref SpanByte input, ref SpanByteAndMemory output, ref TContext context, RespCommand cmd) - where TContext : ITsavoriteContext + public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, ref TContext context, RespCommand cmd) + where TContext : ITsavoriteContext { var status = context.RMW(ref key, ref input, ref output); @@ -599,10 +570,9 @@ public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice, { return RENAME(oldKeySlice, newKeySlice, storeType, true, out result); } - private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool isNX, out int result) { - RawStringInput input = default; + RawStringInput input = new RawStringInput(RespCommand.GETWITHETAG); var returnStatus = GarnetStatus.NOTFOUND; result = -1; @@ -630,50 +600,31 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S { try { - // GET with etag to find if the key alrady exists, and if it exists then we can check if it also has an etag - var inputSize = sizeof(int) + RespInputHeader.Size; - SpanByte getWithEtagInput = SpanByte.Reinterpret(stackalloc byte[inputSize]); - byte* inputPtr = getWithEtagInput.ToPointer(); - ((RespInputHeader*)inputPtr)->cmd = RespCommand.GETWITHETAG; - ((RespInputHeader*)inputPtr)->flags = 0; + var newKey = newKeySlice.SpanByte; - SpanByteAndMemory etagAndDataOutput = new SpanByteAndMemory(); - GarnetStatus status = GET(ref oldKey, ref getWithEtagInput, ref etagAndDataOutput, ref context); + var o = new SpanByteAndMemory(); + var status = GET(ref oldKey, ref input, ref o, ref context); if (status == GarnetStatus.OK) { + Debug.Assert(!o.IsSpanByte); + var memoryHandle = o.Memory.Memory.Pin(); + var ptrVal = (byte*)memoryHandle.Pointer; + var end = ptrVal + o.Length; - bool hasEtag = false; + // read array metadata + RespReadUtils.ReadSignedArrayLength(out int numItemsInArr, ref ptrVal, end); - // since we didn't give the etagAndDataOutput span any memory when creating it, the backend would necessarily have had to allocate heap memory if item is not NOTFOUND - Debug.Assert(!etagAndDataOutput.IsSpanByte); - using MemoryHandle outputMemHandle = etagAndDataOutput.Memory.Memory.Pin(); - byte* outputBufCurr = (byte*)outputMemHandle.Pointer; - byte* end = outputBufCurr + etagAndDataOutput.Length; + Debug.Assert(numItemsInArr == 2, "Items in GETWITHETAG response array should always be 2"); - // GETWITHETAG returns an array of two items, etag, and value. - // we need to read past RESP metadata and control sequences to get to etag, and value - RespReadUtils.ReadUnsignedArrayLength(out int numItemInArr, ref outputBufCurr, end); - - Debug.Assert(numItemInArr == 2, "GETWITHETAG output RESP array should be of 2 elements only."); - - // we know a key-val pair does not have an etag if the first element is not null - // if read nil is successful it will point to the ptrVal otherwise we need to re-read past the etag as RESP int64 - byte* startOfEtagPtr = outputBufCurr; - hasEtag = !RespReadUtils.ReadNil(ref outputBufCurr, end, out _); - if (hasEtag) + bool oldKeyHadEtag = !RespReadUtils.ReadNil(ref ptrVal, end, out var _); + if (oldKeyHadEtag) { - // read past the etag so we can then get to the ptrVal, we don't need specific val of etag, just need to move past it if it exists - bool etagReadingSuccessful = RespReadUtils.Read64Int(out _, ref startOfEtagPtr, end); - Debug.Assert(etagReadingSuccessful, "Etag should have been read succesffuly"); - // now startOfEtagPtr is past the etag and points to value - outputBufCurr = startOfEtagPtr; + RespReadUtils.Read64Int(out long _, ref ptrVal, end); } - // get length of value from the header - RespReadUtils.ReadUnsignedLengthHeader(out int headerLength, ref outputBufCurr, end); - // outputBuf now points to start of value - byte* ptrVal = outputBufCurr; + // read length of val + RespReadUtils.ReadUnsignedLengthHeader(out var headerLength, ref ptrVal, ptrVal + o.Length); // Find expiration time of the old key var expireSpan = new SpanByteAndMemory(); @@ -690,130 +641,146 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S input = new RawStringInput(RespCommand.SETEXNX); // If the key has an expiration, set the new key with the expiration - if (!hasEtag) + if (expireTimeMs > 0) { - if (expireTimeMs > 0) + if (isNX && !oldKeyHadEtag) { - if (isNX) - { - // Move payload forward to make space for RespInputHeader and Metadata - var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength)); - var setValueSpan = setValue.SpanByte; - var setValuePtr = setValueSpan.ToPointerWithMetadata(); - setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; - ((RespInputHeader*)(setValuePtr + sizeof(long)))->cmd = RespCommand.SETEXNX; - ((RespInputHeader*)(setValuePtr + sizeof(long)))->flags = 0; - var newKey = newKeySlice.SpanByte; - var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context); - - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - returnStatus = GarnetStatus.OK; - } - else - { - SETEX(newKeySlice, new ArgSlice(ptrVal, headerLength), TimeSpan.FromMilliseconds(expireTimeMs), ref context); - } + parseState.InitializeWithArgument(newValSlice); + input.parseState = parseState; + input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; + + var setStatus = SET_Conditional(ref newKey, ref input, ref context); + + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; + returnStatus = GarnetStatus.OK; } - else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key + else { - if (isNX) + if (!isNX && !oldKeyHadEtag) { - // Move payload forward to make space for RespInputHeader - var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size, new ArgSlice(ptrVal, headerLength)); - var setValueSpan = setValue.SpanByte; - var setValuePtr = setValueSpan.ToPointerWithMetadata(); - ((RespInputHeader*)setValuePtr)->cmd = RespCommand.SETEXNX; - ((RespInputHeader*)setValuePtr)->flags = 0; - var newKey = newKeySlice.SpanByte; - var setStatus = SET_Conditional(ref newKey, ref setValueSpan, ref context); - - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; + SETEX(newKeySlice, newValSlice, TimeSpan.FromMilliseconds(expireTimeMs), ref context); + goto POSTINSERTIONS; + } + + /* + This block here on handles combinations: (IsNX && oldKeyHadEtag and !isNX && oldKEyHadEtag) + regardless of whether or not it isNX we know oldKeyHadEtag is true, so we must always use SETWITHETAG. + IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newkey didnt already exist + */ + + // we need to check if old key exists, and if it exists does it have an etag + input.header.cmd = RespCommand.GETWITHETAG; + var getNewKeyWithEtagOutput = new SpanByteAndMemory(); + GarnetStatus getNewKeyWithEtagStatus = GET(ref oldKey, ref input, ref getNewKeyWithEtagOutput, ref context); + + // if it exists and the command is for isNX we need to early exit + if (isNX && getNewKeyWithEtagStatus == GarnetStatus.OK) + { + result = 0; returnStatus = GarnetStatus.OK; + getNewKeyWithEtagOutput.Memory.Dispose(); + goto POSTINSERTIONS; } - else + + // we know this isNX is false inside of the following block, here we need to check if it had an etag + if (getNewKeyWithEtagStatus == GarnetStatus.OK) { - var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); - SET(ref newKey, ref value, ref context); + // if the new key has an etag we want to retain it + using MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin(); + byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; + byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; + + // skip past the array metadata + RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); + + // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set + if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) + { + input.header.SetRetainEtagFlag(); + } } + + // SETWITHETAG newkey valueFromOldkey with expiration + // since we moved from an oldkey to newkey we reset the etag on the new key + parseState.InitializeWithArgument(newValSlice); + input.header.cmd = RespCommand.SETWITHETAG; + input.parseState = parseState; + input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; + + returnStatus = SET_Conditional(ref newKey, ref input, ref context); + result = isNX ? 1 : 0; + getNewKeyWithEtagOutput.Memory.Dispose(); } } - else if ( - (expireTimeMs == -1 || expireTimeMs > 0) && - hasEtag) + else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key { - // IsNX means if the newKey Exists do not set it - // We can't use SET with not exist here so instead we will do an Exists and skip the seeting if exists - bool newKeyAlreadyExists = EXISTS(newKeySlice, storeType, ref context, ref objectContext) == GarnetStatus.OK; - - if (isNX && newKeyAlreadyExists) + if (isNX && !oldKeyHadEtag) { - // Skip setting the new key and go to calling the part after that - result = 0; + // Build parse state + parseState.InitializeWithArgument(newValSlice); + input.parseState = parseState; + + var setStatus = SET_Conditional(ref newKey, ref input, ref context); + + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; returnStatus = GarnetStatus.OK; } else { - var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); - var initialValueSize = value.Length; - - var valPtr = value.ToPointer(); + if (!isNX && !oldKeyHadEtag) + { + var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); + SET(ref newKey, ref value, ref context); + goto POSTINSERTIONS; + } - SpanByte key = newKeySlice.SpanByte; + // we need to check if old key exists, and if it doesnt exist we can proceed with setting new key and expiry + input.header.cmd = RespCommand.GETWITHETAG; + var getNewKeyWithEtagOutput = new SpanByteAndMemory(); + GarnetStatus getNewKeyWithEtagStatus = GET(ref oldKey, ref input, ref getNewKeyWithEtagOutput, ref context); - GarnetStatus setStatus; - if (expireTimeMs == -1) // no expiration provided + // if it exists and the command is for isNX we need to early exit + if (isNX && getNewKeyWithEtagStatus == GarnetStatus.OK) { - /* - * Make Space for Resp Input Header Behind The valPtr: - * This will not underflow because SpanByte is being created from pinnned pointer on output buffer that had etag and control sequences behind ptrVal. - * we need 6 bytes behind valPtr that we know exists because even in worst case where we only an etag of 0 (1 byte in resp output buffer), the output - * buffer will be of structure: - * *2\r\n - * :0\r\n - * <_VALUE_>\r\n - * this gives us 6 bytes behind it in pinned memory that we can borrow, and 2 bytes infront of value - */ - valPtr -= RespInputHeader.Size + sizeof(int); - // set the length - *(int*)valPtr = RespInputHeader.Size + value.Length; - ((RespInputHeader*)(valPtr + sizeof(int)))->cmd = RespCommand.SETWITHETAG; - ((RespInputHeader*)(valPtr + sizeof(int)))->flags = 0; - // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag - // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key - ((RespInputHeader*)(valPtr + sizeof(int)))->SetRetainEtagFlag(); - - setStatus = SET_Conditional(ref key, ref Unsafe.AsRef(valPtr), ref context); + result = 0; + returnStatus = GarnetStatus.OK; + goto POSTINSERTIONS; } - else + + // we know this isNX is false inside of the following block, here we need to check if it had an etag + if (getNewKeyWithEtagStatus == GarnetStatus.OK) { - // make space for metadata to be added to valPtr - var setValue = scratchBufferManager.FormatScratch(RespInputHeader.Size + sizeof(long), new ArgSlice(ptrVal, headerLength)); - var setValueSpan = setValue.SpanByte; - var setValuePtr = setValueSpan.ToPointerWithMetadata(); - ((RespInputHeader*)setValuePtr + sizeof(long))->cmd = RespCommand.SETWITHETAG; - ((RespInputHeader*)setValuePtr + sizeof(long))->flags = 0; - // This handles the edge case where we are renaming to a key that already exists and has an etag we want to retain its existing etag - // if there wasn't already an existing key same as the "rename to" key or without an etag, this will initialize the etag to 0 on the renamed key - ((RespInputHeader*)setValuePtr)->SetRetainEtagFlag(); - - setValueSpan.ExtraMetadata = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; - - setStatus = SET_Conditional(ref key, ref setValueSpan, ref context); + // if the new key has an etag we want to retain it + using MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin(); + byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; + byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; + + // skip past the array metadata + RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); + + // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set + if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) + { + input.header.SetRetainEtagFlag(); + } } + // SETWITHETAG newkey valueFromOldkey with expiration + // since we moved from an oldkey to newkey we reset the etag on the new key + parseState.InitializeWithArgument(newValSlice); + input.header.cmd = RespCommand.SETWITHETAG; + input.parseState = parseState; - // isNx or/and new key does not exist, either way we can set result to 1 result var will only get used if !isNx, and otherwise is ignored. - // Setting result regardless will avoid the need to add branching here - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - - returnStatus = GarnetStatus.OK; + returnStatus = SET_Conditional(ref newKey, ref input, ref context); + result = isNX ? 1 : 0; } } - etagAndDataOutput.Memory.Dispose(); + POSTINSERTIONS: expireSpan.Memory.Dispose(); + memoryHandle.Dispose(); + o.Memory.Dispose(); // Delete the old key only when SET NX succeeded if (isNX && result == 1) diff --git a/libs/server/Transaction/TransactionManager.cs b/libs/server/Transaction/TransactionManager.cs index cbdeb5cae2..1119cf84db 100644 --- a/libs/server/Transaction/TransactionManager.cs +++ b/libs/server/Transaction/TransactionManager.cs @@ -197,7 +197,7 @@ internal bool RunTransactionProc(byte id, ref CustomProcedureInput procInput, Cu // Log the transaction to AOF Log(id, ref procInput); - // Commit + // Transaction Commit Commit(); } catch (Exception ex) diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 6eaa82f1d2..c5ba6a9d5d 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -894,7 +894,7 @@ public void SimpleIncrementByFloatForEtagSetData(double initialValue, double inc Assert.That(actualResult, Is.EqualTo(expectedResult).Within(1.0 / Math.Pow(10, 15))); Assert.That(actualResult, Is.EqualTo(actualResultRaw).Within(1.0 / Math.Pow(10, 15))); - + RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", key); long etag = (long)res[0]; double value = double.Parse(res[1].ToString(), CultureInfo.InvariantCulture); @@ -2049,9 +2049,11 @@ public void BitFieldIncrementWithSaturateOverflowOnEtagSetData() db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte // Act - Set initial value to 250 and try to increment by 10 with saturate overflow - db.Execute("BITFIELD", key, "SET", "u8", "0", "250"); + var bitfieldRes = db.Execute("BITFIELD", key, "SET", "u8", "0", "250"); + ClassicAssert.AreEqual(0, (long)bitfieldRes); - long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); + var result = (RedisResult[])db.Execute("GETWITHETAG", [key]); + long etagToCheck = long.Parse(result[0].ToString()); ClassicAssert.AreEqual(1, etagToCheck); var incrResult = db.Execute("BITFIELD", key, "OVERFLOW", "SAT", "INCRBY", "u8", "0", "10"); From c832f719cb6806c31ef9f4407cfaa47f04a3714e Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 16 Dec 2024 09:50:33 -0800 Subject: [PATCH 29/87] fmt --- libs/server/Resp/Bitmap/BitmapManagerBitPos.cs | 2 +- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 1 + libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs | 2 +- test/Garnet.test/GarnetBitmapTests.cs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index 950f098f9d..753ef1331f 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -175,7 +175,7 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long // if we are exceeding it, return -1 if (pos >= len * 8) - return -1; + return -1; if (payload == mask) return pos + 0; diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 0f730c8935..1fe9005607 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -285,6 +285,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } + // HK TODO: Should this be updating etag? return true; case RespCommand.SETWITHETAG: // SETWITHETAG WILL OVERRIDE the existing value, unless sent with RETAIN ETAG and already has etag diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 4a9488cce8..a8f57b343b 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -216,7 +216,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b case RespCommand.SETEXXX: if (!retainEtag) etagOffset = 0; - return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + etagOffset; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + etagOffset; case RespCommand.PERSIST: return sizeof(int) + t.LengthWithoutMetadata; case RespCommand.SETWITHETAG: diff --git a/test/Garnet.test/GarnetBitmapTests.cs b/test/Garnet.test/GarnetBitmapTests.cs index 3e24274a1e..b89053f213 100644 --- a/test/Garnet.test/GarnetBitmapTests.cs +++ b/test/Garnet.test/GarnetBitmapTests.cs @@ -686,7 +686,7 @@ public void BitmapBitPosTest_BoundaryConditions() ClassicAssert.IsTrue(db.StringSet(key, bitmap)); // first unset bit, should increment - for (int i = 0; i < bitmapSize; i ++) + for (int i = 0; i < bitmapSize; i++) { // first unset bit ClassicAssert.AreEqual(i, db.StringBitPosition(key, false)); From e7a90c8db244d3ce11db8bd94cc333c1468e09cd Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 16 Dec 2024 14:39:36 -0800 Subject: [PATCH 30/87] Fix boundary condition for bitmanager --- libs/server/Resp/Bitmap/BitmapManagerBitPos.cs | 18 ++++++++++-------- test/Garnet.test/GarnetBitmapTests.cs | 4 +++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index 753ef1331f..8657c00b57 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System; using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.Intrinsics.X86; namespace Garnet.server @@ -94,7 +97,9 @@ public static long BitPosDriver(byte setVal, long startOffset, long endOffset, b return -1; endOffset = endOffset >= valLen ? valLen : endOffset; - return BitPosByte(value, setVal, startOffset, endOffset); + long pos = BitPosByte(value, setVal, startOffset, endOffset); + // check if position is exceeding the last byte in acceptable range + return pos >= ((endOffset + 1) * 8) ? -1 : pos; } startOffset = startOffset < 0 ? ProcessNegativeOffset(startOffset, valLen * 8) : startOffset; @@ -119,7 +124,8 @@ public static long BitPosDriver(byte setVal, long startOffset, long endOffset, b var _startOffset = (startOffset / 8) + 1; var _endOffset = (endOffset / 8) - 1; var _bpos = BitPosByte(value, setVal, _startOffset, _endOffset); - if (_bpos != -1) return _bpos; + + if (_bpos != -1 && _bpos < (_endOffset + 1) * 8) return _bpos; // Search suffix var _spos = BitPosIndexBitSearch(value, setVal, endOffset); @@ -171,15 +177,11 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long // Transform to count leading zeros payload = (bSetVal == 0) ? ~payload : payload; - pos += (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); - - // if we are exceeding it, return -1 - if (pos >= len * 8) - return -1; - if (payload == mask) return pos + 0; + pos += (long)BitOperations.LeadingZeroCount((ulong)payload); + return pos; } } diff --git a/test/Garnet.test/GarnetBitmapTests.cs b/test/Garnet.test/GarnetBitmapTests.cs index b89053f213..627a4984d4 100644 --- a/test/Garnet.test/GarnetBitmapTests.cs +++ b/test/Garnet.test/GarnetBitmapTests.cs @@ -165,7 +165,6 @@ public void BitmapSimpleSetGet_PCT(int bytesPerSend) public void BitmapSetGetBitTest_LTM(bool preSet) { int bitmapBytes = 512; - server.Start(); using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -2292,6 +2291,9 @@ public void BitmapBitPosFixedTests() pos = db.StringBitPosition(key, true, 10, 12, StringIndexType.Bit); ClassicAssert.AreEqual(10, pos); + pos = db.StringBitPosition(key, true, 20, 25, StringIndexType.Bit); + ClassicAssert.AreEqual(-1, pos); + key = "mykey2"; db.StringSetBit(key, 63, false); pos = db.StringBitPosition(key, false, 1); From 56e305c63591401858e0d61cf929c141a78767f8 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 16 Dec 2024 15:42:40 -0800 Subject: [PATCH 31/87] fix rename edgecase --- .../Storage/Session/MainStore/MainStoreOps.cs | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 57fc9bf830..f06986e64f 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -672,7 +672,7 @@ IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newke // we need to check if old key exists, and if it exists does it have an etag input.header.cmd = RespCommand.GETWITHETAG; var getNewKeyWithEtagOutput = new SpanByteAndMemory(); - GarnetStatus getNewKeyWithEtagStatus = GET(ref oldKey, ref input, ref getNewKeyWithEtagOutput, ref context); + GarnetStatus getNewKeyWithEtagStatus = GET(ref newKey, ref input, ref getNewKeyWithEtagOutput, ref context); // if it exists and the command is for isNX we need to early exit if (isNX && getNewKeyWithEtagStatus == GarnetStatus.OK) @@ -687,18 +687,22 @@ IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newke if (getNewKeyWithEtagStatus == GarnetStatus.OK) { // if the new key has an etag we want to retain it - using MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin(); - byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; - byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; + using (MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin()) + { + byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; + byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; - // skip past the array metadata - RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); + // skip past the array metadata + RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); - // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set - if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) - { - input.header.SetRetainEtagFlag(); + // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set + if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) + { + input.header.SetRetainEtagFlag(); + } } + + getNewKeyWithEtagOutput.Memory.Dispose(); } // SETWITHETAG newkey valueFromOldkey with expiration @@ -708,9 +712,11 @@ IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newke input.parseState = parseState; input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; - returnStatus = SET_Conditional(ref newKey, ref input, ref context); + var tempOutput = new SpanByteAndMemory(); + SET_Conditional(ref newKey, ref input, ref tempOutput, ref context, RespCommand.SETWITHETAG); + returnStatus = GarnetStatus.OK; + result = isNX ? 1 : 0; - getNewKeyWithEtagOutput.Memory.Dispose(); } } else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key @@ -739,7 +745,7 @@ IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newke // we need to check if old key exists, and if it doesnt exist we can proceed with setting new key and expiry input.header.cmd = RespCommand.GETWITHETAG; var getNewKeyWithEtagOutput = new SpanByteAndMemory(); - GarnetStatus getNewKeyWithEtagStatus = GET(ref oldKey, ref input, ref getNewKeyWithEtagOutput, ref context); + GarnetStatus getNewKeyWithEtagStatus = GET(ref newKey, ref input, ref getNewKeyWithEtagOutput, ref context); // if it exists and the command is for isNX we need to early exit if (isNX && getNewKeyWithEtagStatus == GarnetStatus.OK) @@ -753,18 +759,22 @@ IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newke if (getNewKeyWithEtagStatus == GarnetStatus.OK) { // if the new key has an etag we want to retain it - using MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin(); - byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; - byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; + using (MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin()) + { + byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; + byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; - // skip past the array metadata - RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); + // skip past the array metadata + RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); - // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set - if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) - { - input.header.SetRetainEtagFlag(); + // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set + if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) + { + input.header.SetRetainEtagFlag(); + } } + + getNewKeyWithEtagOutput.Memory.Dispose(); } // SETWITHETAG newkey valueFromOldkey with expiration @@ -773,7 +783,10 @@ IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newke input.header.cmd = RespCommand.SETWITHETAG; input.parseState = parseState; - returnStatus = SET_Conditional(ref newKey, ref input, ref context); + var tempOutput = new SpanByteAndMemory(); + SET_Conditional(ref newKey, ref input, ref tempOutput, ref context, RespCommand.SETWITHETAG); + returnStatus = GarnetStatus.OK; + result = isNX ? 1 : 0; } } From ddf63ca71618bc47e62093ba41be4bec3a1f3340 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 17 Dec 2024 01:17:11 -0800 Subject: [PATCH 32/87] Add custom txn, proc, and cmd handling --- libs/server/Custom/CustomFunctions.cs | 11 ++ libs/server/Resp/CmdStrings.cs | 1 + .../Storage/Functions/MainStore/RMWMethods.cs | 14 +- .../Storage/Session/MainStore/MainStoreOps.cs | 1 - test/Garnet.test/RespCustomCommandTests.cs | 165 ++++++++++++++++++ 5 files changed, 190 insertions(+), 2 deletions(-) diff --git a/libs/server/Custom/CustomFunctions.cs b/libs/server/Custom/CustomFunctions.cs index b9ce4f8a01..df5976baf1 100644 --- a/libs/server/Custom/CustomFunctions.cs +++ b/libs/server/Custom/CustomFunctions.cs @@ -116,6 +116,17 @@ protected static unsafe void WriteBulkStringArray(ref MemoryResult output, } } + /// + /// Create output as bulk string, from given Span + /// + protected static unsafe void WriteBulkString(ref MemoryResult output, Span simpleString) + { + var _output = (output.MemoryOwner, output.Length); + WriteBulkString(ref _output, simpleString); + output.MemoryOwner = _output.MemoryOwner; + output.Length = _output.Length; + } + /// /// Create output as bulk string, from given Span /// diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index adff970f4a..4535924996 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -148,6 +148,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_WRONG_TYPE => "WRONGTYPE Operation against a key holding the wrong kind of value."u8; public static ReadOnlySpan RESP_ERR_WRONG_TYPE_HLL => "WRONGTYPE Key is not a valid HyperLogLog string value."u8; public static ReadOnlySpan RESP_ERR_EXEC_ABORT => "EXECABORT Transaction discarded because of previous errors."u8; + public static ReadOnlySpan RESP_ERR_ETAG_ON_CUSTOM_PROC => "WRONGTYPE Key with etag cannot be used for custom procedure."u8; /// /// Generic error response strings, i.e. these are of the form "-ERR error message\r\n" diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 1fe9005607..2db651a01c 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -632,6 +632,12 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re default: if (cmd > RespCommandExtensions.LastValidCommand) { + if (recordInfo.ETag) + { + CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); + return true; + } + var functions = functionsState.GetCustomCommandFunctions((ushort)cmd); var expiration = input.arg1; if (expiration == -1) @@ -658,7 +664,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); - var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), ref input, value.AsSpan(), ref valueLength, ref outp, ref rmwInfo); + var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), ref input, value.AsSpan(etagIgnoredOffset), ref valueLength, ref outp, ref rmwInfo); Debug.Assert(valueLength <= value.LengthWithoutMetadata); // Adjust value length if user shrinks it @@ -1045,6 +1051,12 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) { + if (recordInfo.ETag) + { + CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); + return true; + } + var functions = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd); var expiration = input.arg1; if (expiration == 0) diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index f06986e64f..ca6fd0400a 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -1227,7 +1227,6 @@ public unsafe GarnetStatus Increment(ArgSlice key, out long output, lo CompletePendingForSession(ref status, ref _output, ref context); Debug.Assert(_output.IsSpanByte); - Debug.Assert(_output.Length == outputBufferLength); output = NumUtils.BytesToLong(_output.Length, outputBuffer); return GarnetStatus.OK; diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index cef9353517..b86363e0e2 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -141,6 +141,92 @@ public override bool Execute(TGarnetApi garnetApi, ref CustomProcedu } } + public class CustomIncrProc : CustomProcedure + { + public override bool Execute(TGarnetApi garnetApi, ref CustomProcedureInput procInput, ref MemoryResult output) + { + var offset = 0; + var keyToIncrement = GetNextArg(ref procInput, ref offset); + + // should update etag invisibly + garnetApi.Increment(keyToIncrement, out long _, 1); + + var keyToReturn = GetNextArg(ref procInput, ref offset); + garnetApi.GET(keyToReturn, out ArgSlice outval); + WriteBulkString(ref output, outval.Span); + return true; + } + } + + // one txn that works with etag data in multiple ways + public class RandomSubstituteOrExpandValForKeyTxn : CustomTransactionProcedure + { + public override bool Prepare(TGarnetReadApi api, ref CustomProcedureInput procInput) + { + int offset = 0; + AddKey(GetNextArg(ref procInput, ref offset), LockType.Exclusive, false); + AddKey(GetNextArg(ref procInput, ref offset), LockType.Exclusive, false); + return true; + } + + public override unsafe void Main(TGarnetApi garnetApi, ref CustomProcedureInput procInput, ref MemoryResult output) + { + Random rnd = new Random(); + + int offset = 0; + var key = GetNextArg(ref procInput, ref offset); + + // key will have an etag associated with it already but the transaction should not be able to see it. + // if the transaction needs to see it, then it can send GET with cmd as GETWITHETAG + garnetApi.GET(key, out ArgSlice outval); + + List valueToMessWith = outval.ToArray().ToList(); + + // random decision of either substitute, expand, or reduce value + char randChar = (char)('a' + rnd.Next(0, 26)); + int decision = rnd.Next(0, 100); + if (decision < 33) + { + // substitute + int idx = rnd.Next(0, valueToMessWith.Count); + valueToMessWith[idx] = (byte)randChar; + } + else if (decision < 66) + { + valueToMessWith.Add((byte)randChar); + } + else + { + valueToMessWith.RemoveAt(valueToMessWith.Count - 1); + } + + RawStringInput input = new RawStringInput(RespCommand.SET); + input.header.cmd = RespCommand.SET; + // if we send a SET we must explictly ask it to retain etag, and use conditional set + input.header.SetRetainEtagFlag(); + + fixed (byte* valuePtr = valueToMessWith.ToArray()) + { + ArgSlice valForKey1 = new ArgSlice(valuePtr, valueToMessWith.Count); + input.parseState.InitializeWithArgument(valForKey1); + // since we are setting with retain to etag, this change should be reflected in an etag update + SpanByte sameKeyToUse = key.SpanByte; + garnetApi.SET_Conditional(ref sameKeyToUse, ref input); + } + + + var keyToIncrment = GetNextArg(ref procInput, ref offset); + + // for a non SET command the etag should be invisible and be updated automatically + garnetApi.Increment(keyToIncrment, out long _, 1); + } + } + + // HK TODO: + // one custom proc that reads and works with etag data + // assert that all reads inside of a txn or custom for a previously etag are hiding etag + // assert that any writes on the etag set data modified the etag on it + [TestFixture] public class RespCustomCommandTests { @@ -1279,5 +1365,84 @@ public void MultiRegisterProcTest() ClassicAssert.AreEqual("65", retValue.ToString()); } } + + [Test] + public void CustomTxnEtagInteractionTest() + { + server.Register.NewTransactionProc("RANDOPS", () => new RandomSubstituteOrExpandValForKeyTxn()); + + var key1 = "key1"; + var value1 = "thisisstarting"; + + var key2 = "key2"; + var value2 = "17"; + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + try + { + db.Execute("SETWITHETAG", key1, value1); + db.Execute("SETWITHETAG", key2, value2); + + RedisResult result = db.Execute("RANDOPS", key1, key2); + + ClassicAssert.AreEqual("OK", result.ToString()); + + // check GETWITHETAG shows updated etag and expected values for both + RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", key1); + ClassicAssert.AreEqual("1", res[0].ToString()); + ClassicAssert.IsTrue(res[1].ToString().All(c => c - 'a' >= 0 && c - 'a' < 26)); + + res = (RedisResult[])db.Execute("GETWITHETAG", key2); + ClassicAssert.AreEqual("1", res[0].ToString()); + ClassicAssert.AreEqual("18", res[1].ToString()); + } + catch (RedisServerException rse) + { + ClassicAssert.Fail(rse.Message); + } + } + + [Test] + public void CustomProcEtagInteractionTest() + { + server.Register.NewProcedure("INCRGET", () => new CustomIncrProc()); + + var key1 = "key1"; + var value1 = "thisisstarting"; + + var key2 = "key2"; + var value2 = "256"; + + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + try + { + db.Execute("SETWITHETAG", key1, value1); + db.Execute("SETWITHETAG", key2, value2); + + // incr key2, and just get key1 + RedisResult result = db.Execute("INCRGET", key2, key1); + + ClassicAssert.AreEqual(value1, result.ToString()); + + // check GETWITHETAG shows updated etag and expected values for both + RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", key1); + // etag not updated for this + ClassicAssert.AreEqual("0", res[0].ToString()); + ClassicAssert.AreEqual(value1, res[1].ToString()); + + res = (RedisResult[])db.Execute("GETWITHETAG", key2); + // etag updated for this + ClassicAssert.AreEqual("1", res[0].ToString()); + ClassicAssert.AreEqual("257", res[1].ToString()); + } + catch (RedisServerException rse) + { + ClassicAssert.Fail(rse.Message); + } + } } } \ No newline at end of file From 4e327b4746912674ac9190ec6a1ff514d39d74ca Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 17 Dec 2024 01:35:03 -0800 Subject: [PATCH 33/87] update docs --- website/docs/commands/etag-commands.md | 109 ----------------------- website/docs/commands/garnet-specific.md | 103 +++++++++++++++++++++ website/sidebars.js | 2 +- 3 files changed, 104 insertions(+), 110 deletions(-) delete mode 100644 website/docs/commands/etag-commands.md diff --git a/website/docs/commands/etag-commands.md b/website/docs/commands/etag-commands.md deleted file mode 100644 index 7d1cef53be..0000000000 --- a/website/docs/commands/etag-commands.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -id: etag-commands -sidebar_label: ETags -title: ETAG -slug: etag ---- - ---- - -## ETag Support - -Garnet provides support for ETags on raw strings. By using the ETag-related commands outlined below, you can associate any string-based key-value pair inserted into Garnet with an automatically updated ETag. - -Compatibility with non-ETag commands and the behavior of data inserted with ETags are detailed at the end of this document. - ---- - -### **SETWITHETAG** - -#### **Syntax** - -```bash -SETWITHETAG key value [RETAINETAG] -``` - -Inserts a key-value string pair into Garnet, associating an ETag that will be updated upon changes to the value. - -**Options:** - -* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If not etag existed for the previous key this will initialize one. - -#### **Response** - -- **Integer reply**: A response integer indicating the initial ETag value on success. - ---- - -### **GETWITHETAG** - -#### **Syntax** - -```bash -GETWITHETAG key -``` - -Retrieves the value and the ETag associated with the given key. - -#### **Response** - -One of the following: - -- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. If called on a key-value pair without ETag, the first item will be nil. -- **Nil reply**: If the key does not exist. - ---- - -### **SETIFMATCH** - -#### **Syntax** - -```bash -SETIFMATCH key value etag -``` - -Updates the value of a key if the provided ETag matches the current ETag of the key. - -#### **Response** - -One of the following: - -- **Integer reply**: The updated ETag if the value was successfully updated. -- **Nil reply**: If the key does not exist. -- **Simple string reply**: If the provided ETag does not match the current ETag or If the command is called on a record without an ETag a simple string indicating ETag mismatch is returned. - ---- - -### **GETIFNOTMATCH** - -#### **Syntax** - -```bash -GETIFNOTMATCH key etag -``` - -Retrieves the value if the ETag associated with the key has changed; otherwise, returns a response indicating no change. - -#### **Response** - -One of the following: - -- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be nil. -- **Nil reply**: If the key does not exist. -- **Simple string reply**: if the provided ETag matches the current ETag, returns a simple string indicating the value is unchanged. - ---- - -## Compatibility and Behavior with Non-ETag Commands - -Below is the expected behavior of ETag-associated key-value pairs when non-ETag commands are used. - -- **MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. - -- **SET**: Only if used with additional option "RETAINETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. - -- **RENAME**: Renaming an ETag-associated key-value pair will reset the ETag to 0 for the renamed key. Unless the key being renamed to already existed before hand, in that case it will retain the etag of the existing key that was the target of the rename. - -All other commands will update the etag internally if they modify the underlying data, and any responses from them will not expose the etag to the client. To the users the etag and it's updates remain hidden in non-etag commands. - ---- \ No newline at end of file diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index d98b6bcfa0..a3c48615e9 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -122,3 +122,106 @@ initialization code registers all relevant commands and transactions automatical for details. --- + +## Native ETag Support + +Garnet provides support for ETags on raw strings. By using the ETag-related commands outlined below, you can associate any **string based key-value pair** inserted into Garnet with an automatically updated ETag. + +Compatibility with non-ETag commands and the behavior of data inserted with ETags are detailed at the end of this document. + +--- + +### **SETWITHETAG** + +#### **Syntax** + +```bash +SETWITHETAG key value [RETAINETAG] +``` + +Inserts a key-value string pair into Garnet, associating an ETag that will be updated upon changes to the value. + +**Options:** + +* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If not etag existed for the previous key this will initialize one. + +#### **Response** + +- **Integer reply**: A response integer indicating the initial ETag value on success. + +--- + +### **GETWITHETAG** + +#### **Syntax** + +```bash +GETWITHETAG key +``` + +Retrieves the value and the ETag associated with the given key. + +#### **Response** + +One of the following: + +- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. If called on a key-value pair without ETag, the first item will be nil. +- **Nil reply**: If the key does not exist. + +--- + +### **SETIFMATCH** + +#### **Syntax** + +```bash +SETIFMATCH key value etag +``` + +Updates the value of a key if the provided ETag matches the current ETag of the key. + +#### **Response** + +One of the following: + +- **Integer reply**: The updated ETag if the value was successfully updated. +- **Nil reply**: If the key does not exist. +- **Simple string reply**: If the provided ETag does not match the current ETag or If the command is called on a record without an ETag a simple string indicating ETag mismatch is returned. + +--- + +### **GETIFNOTMATCH** + +#### **Syntax** + +```bash +GETIFNOTMATCH key etag +``` + +Retrieves the value if the ETag associated with the key has changed; otherwise, returns a response indicating no change. + +#### **Response** + +One of the following: + +- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be nil. +- **Nil reply**: If the key does not exist. +- **Simple string reply**: if the provided ETag matches the current ETag, returns a simple string indicating the value is unchanged. + +--- + +### Compatibility and Behavior with Non-ETag Commands + +Below is the expected behavior of ETag-associated key-value pairs when non-ETag commands are used. + +- **MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. + +- **SET**: Only if used with additional option "RETAINETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. + +- **RENAME**: Renaming an old ETag-associated key-value pair will create the newly renamed key with an initial etag of 0. If the key being renamed to already existed before hand, it will retain the etag of the existing key that was the target of the rename, and show it as an updated etag. + +- **Custom Commands**: While etag based key value pairs **can be used blindly inside of custom transactions and custom procedures**, ETag set key value pairs are **not supported to be used from inside of Custom Raw String Functions.** + +All other commands will update the etag internally if they modify the underlying data, and any responses from them will not expose the etag to the client. To the users the etag and it's updates remain hidden in non-etag commands. + +--- diff --git a/website/sidebars.js b/website/sidebars.js index 5a888ad629..db3fb0e43c 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -20,7 +20,7 @@ const sidebars = { {type: 'category', label: 'Welcome', collapsed: false, items: ["welcome/intro", "welcome/news", "welcome/features", "welcome/releases", "welcome/compatibility", "welcome/roadmap", "welcome/faq", "welcome/about-us"]}, {type: 'category', label: 'Getting Started', items: ["getting-started/build", "getting-started/configuration", "getting-started/memory", "getting-started/security", "getting-started/compaction"]}, {type: 'category', label: 'Benchmarking', items: ["benchmarking/overview", "benchmarking/results-resp-bench", "benchmarking/resp-bench", {type: 'link', label: 'BDN Charts', href: 'https://microsoft.github.io/garnet/charts/'}]}, - {type: 'category', label: 'Commands', items: ["commands/overview", "commands/api-compatibility", "commands/raw-string", "commands/etag-commands", "commands/generic-commands", "commands/analytics-commands", "commands/data-structures", "commands/server-commands", "commands/client-commands", "commands/checkpoint-commands", "commands/transactions-commands", "commands/cluster", "commands/acl-commands", "commands/scripting-commands", "commands/garnet-specific-commands"]}, + {type: 'category', label: 'Commands', items: ["commands/overview", "commands/api-compatibility", "commands/raw-string", "commands/generic-commands", "commands/analytics-commands", "commands/data-structures", "commands/server-commands", "commands/client-commands", "commands/checkpoint-commands", "commands/transactions-commands", "commands/cluster", "commands/acl-commands", "commands/scripting-commands", "commands/garnet-specific-commands"]}, {type: 'category', label: 'Server Extensions', items: ["extensions/overview", "extensions/raw-strings", "extensions/objects", "extensions/transactions", "extensions/procedure", "extensions/module"]}, {type: 'category', label: 'Cluster Mode', items: ["cluster/overview", "cluster/replication", "cluster/key-migration"]}, {type: 'category', label: 'Developer Guide', items: ["dev/onboarding", "dev/code-structure", "dev/configuration", "dev/network", "dev/processing", "dev/garnet-api", From 1d05e69c57f7134aedc27788a6fb25867cfbb4c9 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 17 Dec 2024 12:41:05 -0800 Subject: [PATCH 34/87] Finish website and nits --- libs/server/Resp/Bitmap/BitmapManagerBitPos.cs | 2 -- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 1 - test/Garnet.test/RespCustomCommandTests.cs | 6 ------ website/docs/commands/raw-string.md | 2 +- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index 8657c00b57..ee16f5e1f4 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; using System.Diagnostics; using System.Numerics; -using System.Runtime.CompilerServices; using System.Runtime.Intrinsics.X86; namespace Garnet.server diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2db651a01c..ad1ffb3628 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -285,7 +285,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - // HK TODO: Should this be updating etag? return true; case RespCommand.SETWITHETAG: // SETWITHETAG WILL OVERRIDE the existing value, unless sent with RETAIN ETAG and already has etag diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index b86363e0e2..487ce23141 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -158,7 +158,6 @@ public override bool Execute(TGarnetApi garnetApi, ref CustomProcedu } } - // one txn that works with etag data in multiple ways public class RandomSubstituteOrExpandValForKeyTxn : CustomTransactionProcedure { public override bool Prepare(TGarnetReadApi api, ref CustomProcedureInput procInput) @@ -222,11 +221,6 @@ public override unsafe void Main(TGarnetApi garnetApi, ref CustomPro } } - // HK TODO: - // one custom proc that reads and works with etag data - // assert that all reads inside of a txn or custom for a previously etag are hiding etag - // assert that any writes on the etag set data modified the etag on it - [TestFixture] public class RespCustomCommandTests { diff --git a/website/docs/commands/raw-string.md b/website/docs/commands/raw-string.md index 3dc85182c8..f869ae4169 100644 --- a/website/docs/commands/raw-string.md +++ b/website/docs/commands/raw-string.md @@ -301,7 +301,7 @@ Set **key** to hold the string value. If key already holds a value, it is overwr * NX -- Only set the key if it does not already exist. * XX -- Only set the key if it already exists. * KEEPTTL -- Retain the time to live associated with the key. -* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this will create the new key-value pair without any etag as well. +* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this will create the new key-value pair without any etag as well. This is a Garnet specific command, you can read more about ETag support [here](../commands/garnet-specific-commands#native-etag-support) #### Resp Reply From c3b2dbdfa825647ab1f66f5ccf748173ebe9f15f Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 17 Dec 2024 13:25:20 -0800 Subject: [PATCH 35/87] nit --- .../Storage/Functions/MainStore/RMWMethods.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index ad1ffb3628..18cb2c9d5b 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -275,7 +275,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re oldEtag = *(long*)value.ToPointer(); } - // First byte of input payload identifies command switch (cmd) { case RespCommand.SETEXNX: @@ -423,7 +422,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re case RespCommand.PEXPIRE: case RespCommand.EXPIRE: - // doesn't update etag var expiryExists = value.MetadataSize > 0; var expiryValue = input.parseState.GetLong(0); @@ -435,6 +433,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; + + // doesn't update etag, since it's only the metadata that was updated return true; ; case RespCommand.PEXPIREAT: case RespCommand.EXPIREAT: @@ -448,6 +448,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; + + // doesn't update etag, since it's only the metadata that was updated return true; case RespCommand.PERSIST: @@ -529,11 +531,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, v, value.Length - etagIgnoredOffset); - if (!overflow) - CopyRespNumber(bitfieldReturnValue, ref output); - else + if (overflow) + { CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + // etag not updated + return true; + } + + CopyRespNumber(bitfieldReturnValue, ref output); break; + case RespCommand.PFADD: v = value.ToPointer(); From 5bd4e7c24b2e8dd00e6c2a9a1acfd43a0f148127 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 19 Dec 2024 10:54:06 -0800 Subject: [PATCH 36/87] big ole refactor --- libs/resources/RespCommandsDocs.json | 15 + libs/resources/RespCommandsInfo.json | 18 +- libs/server/API/GarnetApi.cs | 8 +- libs/server/API/IGarnetApi.cs | 4 +- libs/server/Constants.cs | 2 + libs/server/InputHeader.cs | 13 +- libs/server/Resp/BasicCommands.cs | 150 ++-- libs/server/Resp/CmdStrings.cs | 4 +- libs/server/Resp/KeyAdminCommands.cs | 38 +- libs/server/Resp/Parser/RespCommand.cs | 9 +- libs/server/Resp/RespServerSession.cs | 1 - .../Functions/MainStore/PrivateMethods.cs | 52 +- .../Storage/Functions/MainStore/RMWMethods.cs | 280 +++++--- .../Functions/MainStore/ReadMethods.cs | 79 +- .../Functions/MainStore/VarLenInputMethods.cs | 19 +- .../Storage/Session/MainStore/MainStoreOps.cs | 175 +---- libs/server/Transaction/TxnKeyManager.cs | 1 - .../CommandInfoUpdater/SupportedCommand.cs | 1 - test/Garnet.test/Resp/ACL/RespCommandTests.cs | 15 - test/Garnet.test/RespAofTests.cs | 2 +- test/Garnet.test/RespCustomCommandTests.cs | 10 +- test/Garnet.test/RespEtagTests.cs | 675 +++++++----------- test/Garnet.test/RespTests.cs | 6 +- test/Garnet.test/TransactionTests.cs | 16 +- website/docs/commands/garnet-specific.md | 27 +- website/docs/commands/raw-string.md | 4 +- 26 files changed, 660 insertions(+), 964 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index d9e9fa0b4f..74d14f7792 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -4443,6 +4443,21 @@ "DisplayText": "newkey", "Type": "Key", "KeySpecIndex": 1 + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "CONDITION", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHETAG", + "DisplayText": "WITHETAG", + "Type": "PureToken", + "Token": "WITHETAG" + } + ] } ] }, diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index d402c941f2..c9882467af 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -3009,7 +3009,7 @@ { "Command": "RENAME", "Name": "RENAME", - "Arity": 3, + "Arity": -3, "Flags": "Write", "FirstKey": 1, "LastKey": 2, @@ -3047,7 +3047,7 @@ { "Command": "RENAMENX", "Name": "RENAMENX", - "Arity": 3, + "Arity": -3, "Flags": "Fast, Write", "FirstKey": 1, "LastKey": 2, @@ -3542,20 +3542,6 @@ } ] }, - { - "Command": "SETWITHETAG", - "Name": "SETWITHETAG", - "IsInternal": false, - "Arity": -3, - "Flags": "NONE", - "FirstKey": 1, - "LastKey": 1, - "Step": 1, - "AclCategories": "Fast, String, Write", - "Tips": null, - "KeySpecifications": null, - "SubCommands": null - }, { "Command": "SINTER", "Name": "SINTER", diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index 23120c5ed6..9d1c0cf817 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -176,12 +176,12 @@ public GarnetStatus APPEND(ArgSlice key, ArgSlice value, ref ArgSlice output) #region RENAME /// - public GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All) - => storageSession.RENAME(oldKey, newKey, storeType); + public GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, bool withEtag, StoreType storeType = StoreType.All) + => storageSession.RENAME(oldKey, newKey, storeType, withEtag); /// - public GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, StoreType storeType = StoreType.All) - => storageSession.RENAMENX(oldKey, newKey, storeType, out result); + public GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, bool withEtag, StoreType storeType = StoreType.All) + => storageSession.RENAMENX(oldKey, newKey, storeType, out result, withEtag); #endregion #region EXISTS diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 32606dc73e..82c9de2106 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -131,7 +131,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// /// - GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, StoreType storeType = StoreType.All); + GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, bool withEtag, StoreType storeType = StoreType.All); /// /// Renames key to newkey if newkey does not yet exist. It returns an error when key does not exist. @@ -141,7 +141,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// The result of the operation. /// The type of store to perform the operation on. /// - GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, StoreType storeType = StoreType.All); + GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, bool withEtag, StoreType storeType = StoreType.All); #endregion #region EXISTS diff --git a/libs/server/Constants.cs b/libs/server/Constants.cs index f6e01df9af..c60e4a0adf 100644 --- a/libs/server/Constants.cs +++ b/libs/server/Constants.cs @@ -7,5 +7,7 @@ namespace Garnet.server internal static class Constants { public const int EtagSize = sizeof(long); + + public const int BaseEtag = 0; } } \ No newline at end of file diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index 4e477f9e21..58f1004577 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -29,11 +29,10 @@ public enum RespInputFlags : byte Expired = 128, /// - /// Flag indicating if a SET operation should retain the etag of the previous value if it exists. + /// Flag indicating if a SET operation should either add an etag or respect the etag semantics for a value with an etag already /// This is used for conditional setting. /// - RetainEtag = 129, - + WithEtag = 129, } /// @@ -131,15 +130,15 @@ internal ListOperation ListOp internal unsafe void SetSetGetFlag() => flags |= RespInputFlags.SetGet; /// - /// Set "RetainEtag" flag, used to update the old etag of a key after conditionally setting it + /// Set "WithEtag" flag, used to update the old etag of a key after conditionally setting it /// - internal unsafe void SetRetainEtagFlag() => flags |= RespInputFlags.RetainEtag; + internal unsafe void SetWithEtagFlag() => flags |= RespInputFlags.WithEtag; /// - /// Check if the RetainEtagFlag is set + /// Check if the WithEtag flag is set /// /// - internal unsafe bool CheckRetainEtagFlag() => (flags & RespInputFlags.RetainEtag) != 0; + internal unsafe bool CheckWithEtagFlag() => (flags & RespInputFlags.WithEtag) != 0; /// /// Check if record is expired, either deterministically during log replay, diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ef09e14291..d38b044407 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -390,52 +390,7 @@ private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) var key = parseState.GetArgSliceByRef(0).SpanByte; - // Here Etag retain argument does not really matter because setifmatch may or may not update etag based on the "if match" condition - NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, ref key, getValue: true, highPrecision: false, retainEtag: true, ref storageApi); - - return true; - } - - /// - /// SETWITHETAG key val [RETAINETAG] - /// Sets a key value pair with an ETAG associated with the value internally - /// Calling this on a key that already exists is an error case - /// - private bool NetworkSETWITHETAG(ref TGarnetApi storageApi) - where TGarnetApi : IGarnetApi - { - Debug.Assert(parseState.Count == 2 || parseState.Count == 3); - - var key = parseState.GetArgSliceByRef(0).SpanByte; - - bool retainEtag = false; - if (parseState.Count == 3) - { - Span opt = parseState.GetArgSliceByRef(2).Span; - if (opt.SequenceEqual(CmdStrings.RETAINETAG)) - { - retainEtag = true; - } - else - { - AsciiUtils.ToUpperInPlace(opt); - if (opt.SequenceEqual(CmdStrings.RETAINETAG)) - { - retainEtag = true; - } - else - { - // Unknown option - while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) - SendAndReset(); - return true; - } - - } - } - - // calling set with etag on an exisitng key will update the etag of the existing key - NetworkSET_Conditional(RespCommand.SETWITHETAG, 0, ref key, getValue: true, highPrecision: false, retainEtag, ref storageApi); + NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, ref key, getValue: true, highPrecision: false, withEtag: true, ref storageApi); return true; } @@ -569,10 +524,10 @@ private bool NetworkSETNX(bool highPrecision, ref TGarnetApi storage return NetworkSETEXNX(ref storageApi); } - enum EtagRetentionOption : byte + enum EtagOption : byte { None, - RETAIN, + WITHETAG, } enum ExpirationOption : byte @@ -593,7 +548,7 @@ enum ExistOptions : byte } /// - /// SET EX NX + /// SET EX NX [WITHETAG] /// private bool NetworkSETEXNX(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi @@ -608,7 +563,7 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) ReadOnlySpan errorMessage = default; var existOptions = ExistOptions.None; var expOption = ExpirationOption.None; - var retainEtagOption = EtagRetentionOption.None; + var etagOption = EtagOption.None; var getValue = false; var tokenIdx = 2; @@ -697,16 +652,31 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) } else if (nextOpt.SequenceEqual(CmdStrings.GET)) { + if (etagOption != EtagOption.None) + { + // cannot do withEtag and getValue since withEtag SET already returns ETag in response + errorMessage = CmdStrings.RESP_ERR_WITHETAG_AND_GETVALUE; + break; + } + getValue = true; } - else if (nextOpt.SequenceEqual(CmdStrings.RETAINETAG)) + else if (nextOpt.SequenceEqual(CmdStrings.WITHETAG)) { - if (retainEtagOption != EtagRetentionOption.None) + if (etagOption != EtagOption.None) { errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; break; } - retainEtagOption = EtagRetentionOption.RETAIN; + + if (getValue) + { + // cannot do withEtag and getValue since withEtag SET already returns ETag in response + errorMessage = CmdStrings.RESP_ERR_WITHETAG_AND_GETVALUE; + break; + } + + etagOption = EtagOption.WITHETAG; } else { @@ -735,65 +705,30 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) var valPtr = sbVal.ToPointer() - sizeof(int); var vSize = sbVal.Length; - bool isEtagRetained = retainEtagOption == EtagRetentionOption.RETAIN; + bool withEtag = etagOption == EtagOption.WITHETAG; + + var isHighPrecision = expOption == ExpirationOption.PX; switch (expOption) { case ExpirationOption.None: case ExpirationOption.EX: - switch (existOptions) - { - case ExistOptions.None: - if (isEtagRetained) - { - // cannot do blind upsert if isEtagRetained - return NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, - highPrecision: false, retainEtag: true, storageApi: ref storageApi); - } - else - { - return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue: true, - highPrecision: false, retainEtag: false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, - ref storageApi); // Can perform a blind update - } - case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, - getValue, highPrecision: false, isEtagRetained, ref storageApi); - case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, highPrecision: false, isEtagRetained, ref storageApi); - } - - break; case ExpirationOption.PX: switch (existOptions) { case ExistOptions.None: - if (isEtagRetained) - { - // cannot do a blind update - return NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, - highPrecision: true, retainEtag: true, ref storageApi); - } - else - { - return getValue - ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue: true, - highPrecision: true, retainEtag: false, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update - } + return getValue || withEtag + ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, + isHighPrecision, withEtag, ref storageApi) + : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, - getValue, highPrecision: true, isEtagRetained, ref storageApi); + getValue, highPrecision: isHighPrecision, withEtag, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, highPrecision: true, isEtagRetained, ref storageApi); + getValue, highPrecision: isHighPrecision, withEtag, ref storageApi); } - break; - case ExpirationOption.KEEPTTL: Debug.Assert(expiry == 0); // no expiration if KEEPTTL switch (existOptions) @@ -801,13 +736,13 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) case ExistOptions.None: // We can never perform a blind update due to KEEPTTL return NetworkSET_Conditional(RespCommand.SETKEEPTTL, expiry, ref sbKey - , getValue, highPrecision: false, isEtagRetained, ref storageApi); + , getValue, highPrecision: false, withEtag, ref storageApi); case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETKEEPTTLXX, expiry, ref sbKey, - getValue, highPrecision: false, isEtagRetained, ref storageApi); + getValue, highPrecision: false, withEtag, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, highPrecision: false, isEtagRetained, ref storageApi); + getValue, highPrecision: false, withEtag, ref storageApi); } break; @@ -839,7 +774,7 @@ private unsafe bool NetworkSET_EX(RespCommand cmd, ExpirationOption return true; } - private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref SpanByte key, bool getValue, bool highPrecision, bool retainEtag, ref TGarnetApi storageApi) + private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref SpanByte key, bool getValue, bool highPrecision, bool withEtag, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { var inputArg = expiry == 0 @@ -851,20 +786,21 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref var input = new RawStringInput(cmd, ref parseState, startIdx: 1, arg1: inputArg); - if (retainEtag) - input.header.SetRetainEtagFlag(); - + if (withEtag) + input.header.SetWithEtagFlag(); + if (getValue) input.header.SetSetGetFlag(); - if (getValue) + // getValue and withEtag both need to write to memory something from the record + if (getValue || withEtag) { var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); var status = storageApi.SET_Conditional(ref key, ref input, ref o, cmd); - // not found for a setwithetag is okay, and so we invert it - if (cmd == RespCommand.SETWITHETAG && status == GarnetStatus.NOTFOUND) + // not found for a withEtag based set is okay, and so we invert it + if (withEtag && status == GarnetStatus.NOTFOUND) { status = GarnetStatus.OK; } diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index e76ec6a0d8..72288fea79 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -98,7 +98,7 @@ static partial class CmdStrings public static ReadOnlySpan KEEPTTL => "KEEPTTL"u8; public static ReadOnlySpan NX => "NX"u8; public static ReadOnlySpan XX => "XX"u8; - public static ReadOnlySpan RETAINETAG => "RETAINETAG"u8; + public static ReadOnlySpan WITHETAG => "WITHETAG"u8; public static ReadOnlySpan UNSAFETRUNCATELOG => "UNSAFETRUNCATELOG"u8; public static ReadOnlySpan SAMPLES => "SAMPLES"u8; public static ReadOnlySpan RANK => "RANK"u8; @@ -141,7 +141,6 @@ static partial class CmdStrings public static ReadOnlySpan RESP_PONG => "+PONG\r\n"u8; public static ReadOnlySpan RESP_EMPTY => "$0\r\n\r\n"u8; public static ReadOnlySpan RESP_QUEUED => "+QUEUED\r\n"u8; - public static ReadOnlySpan RESP_VALNOTCHANGED => "+NOTCHANGED\r\n"u8; public static ReadOnlySpan RESP_ETAGMISMTACH => "+ETAGMISMATCH\r\n"u8; /// @@ -168,6 +167,7 @@ static partial class CmdStrings public static ReadOnlySpan RESP_ERR_GENERIC_WATCH_IN_MULTI => "ERR WATCH inside MULTI is not allowed"u8; public static ReadOnlySpan RESP_ERR_GENERIC_INVALIDEXP_IN_SET => "ERR invalid expire time in 'set' command"u8; public static ReadOnlySpan RESP_ERR_GENERIC_SYNTAX_ERROR => "ERR syntax error"u8; + public static ReadOnlySpan RESP_ERR_WITHETAG_AND_GETVALUE => "ERR WITHETAG option not allowed with GET inside of SET"u8; public static ReadOnlySpan RESP_ERR_GENERIC_OFFSETOUTOFRANGE => "ERR offset is out of range"u8; public static ReadOnlySpan RESP_ERR_GENERIC_BIT_IS_NOT_INTEGER => "ERR bit is not an integer or out of range"u8; public static ReadOnlySpan RESP_ERR_GENERIC_BITOFFSET_IS_NOT_INTEGER => "ERR bit offset is not an integer or out of range"u8; diff --git a/libs/server/Resp/KeyAdminCommands.cs b/libs/server/Resp/KeyAdminCommands.cs index 8cf1434517..1ce25dd487 100644 --- a/libs/server/Resp/KeyAdminCommands.cs +++ b/libs/server/Resp/KeyAdminCommands.cs @@ -17,14 +17,29 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase private bool NetworkRENAME(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - if (parseState.Count != 2) + // one optional command for with etag + if (parseState.Count < 2 || parseState.Count > 3) { return AbortWithWrongNumberOfArguments(nameof(RespCommand.RENAME)); } var oldKeySlice = parseState.GetArgSliceByRef(0); var newKeySlice = parseState.GetArgSliceByRef(1); - var status = storageApi.RENAME(oldKeySlice, newKeySlice); + + var withEtag = false; + if (parseState.Count == 3) + { + if (!parseState.GetArgSliceByRef(2).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.WITHETAG)) + { + while (!RespWriteUtils.WriteError($"ERR Unsupported option {parseState.GetString(2)}", ref dcurr, dend)) + SendAndReset(); + return true; + } + + withEtag = true; + } + + var status = storageApi.RENAME(oldKeySlice, newKeySlice, withEtag); switch (status) { @@ -46,14 +61,29 @@ private bool NetworkRENAME(ref TGarnetApi storageApi) private bool NetworkRENAMENX(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - if (parseState.Count != 2) + // one optional command for with etag + if (parseState.Count < 2 || parseState.Count > 3) { return AbortWithWrongNumberOfArguments(nameof(RespCommand.RENAMENX)); } var oldKeySlice = parseState.GetArgSliceByRef(0); var newKeySlice = parseState.GetArgSliceByRef(1); - var status = storageApi.RENAMENX(oldKeySlice, newKeySlice, out var result); + + var withEtag = false; + if (parseState.Count == 3) + { + if (!parseState.GetArgSliceByRef(2).ReadOnlySpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.WITHETAG)) + { + while (!RespWriteUtils.WriteError($"ERR Unsupported option {parseState.GetString(2)}", ref dcurr, dend)) + SendAndReset(); + return true; + } + + withEtag = true; + } + + var status = storageApi.RENAMENX(oldKeySlice, newKeySlice, out var result, withEtag); if (status == GarnetStatus.OK) { diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 405ed6aefe..f605018586 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -157,7 +157,6 @@ public enum RespCommand : ushort SETKEEPTTL, SETKEEPTTLXX, SETRANGE, - SETWITHETAG, SINTERSTORE, SMOVE, SPOP, @@ -677,8 +676,6 @@ private RespCommand FastParseCommand(out int count) (2 << 4) | 5 when lastWord == MemoryMarshal.Read("\nPFADD\r\n"u8) => RespCommand.PFADD, (2 << 4) | 6 when lastWord == MemoryMarshal.Read("INCRBY\r\n"u8) => RespCommand.INCRBY, (2 << 4) | 6 when lastWord == MemoryMarshal.Read("DECRBY\r\n"u8) => RespCommand.DECRBY, - (2 << 4) | 6 when lastWord == MemoryMarshal.Read("RENAME\r\n"u8) => RespCommand.RENAME, - (2 << 4) | 8 when lastWord == MemoryMarshal.Read("NAMENX\r\n"u8) && *(ushort*)(ptr + 8) == MemoryMarshal.Read("RE"u8) => RespCommand.RENAMENX, (2 << 4) | 6 when lastWord == MemoryMarshal.Read("GETBIT\r\n"u8) => RespCommand.GETBIT, (2 << 4) | 6 when lastWord == MemoryMarshal.Read("APPEND\r\n"u8) => RespCommand.APPEND, (2 << 4) | 6 when lastWord == MemoryMarshal.Read("GETSET\r\n"u8) => RespCommand.GETSET, @@ -695,6 +692,8 @@ private RespCommand FastParseCommand(out int count) _ => ((length << 4) | count) switch { // Commands with dynamic number of arguments + >= ((6 << 4) | 2) and <= ((6 << 4) | 3) when lastWord == MemoryMarshal.Read("RENAME\r\n"u8) => RespCommand.RENAME, + >= ((8 << 4) | 2) and <= ((8 << 4) | 3) when lastWord == MemoryMarshal.Read("NAMENX\r\n"u8) && *(ushort*)(ptr + 8) == MemoryMarshal.Read("RE"u8) => RespCommand.RENAMENX, >= ((3 << 4) | 3) and <= ((3 << 4) | 6) when lastWord == MemoryMarshal.Read("3\r\nSET\r\n"u8) => RespCommand.SETEXNX, >= ((5 << 4) | 1) and <= ((5 << 4) | 3) when lastWord == MemoryMarshal.Read("\nGETEX\r\n"u8) => RespCommand.GETEX, >= ((6 << 4) | 0) and <= ((6 << 4) | 9) when lastWord == MemoryMarshal.Read("RUNTXP\r\n"u8) => RespCommand.RUNTXP, @@ -1478,10 +1477,6 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.GETWITHETAG; } - else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nSETWI"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("THETAG\r\n"u8)) - { - return RespCommand.SETWITHETAG; - } else if (*(ulong*)(ptr + 2) == MemoryMarshal.Read("1\r\nPEXPI"u8) && *(ulong*)(ptr + 10) == MemoryMarshal.Read("RETIME\r\n"u8)) { return RespCommand.PEXPIRETIME; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 0cf6a2d1f7..8976b61b6b 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -524,7 +524,6 @@ private bool ProcessBasicCommands(RespCommand cmd, ref TGarnetApi st RespCommand.SETNX => NetworkSETNX(false, ref storageApi), RespCommand.PSETEX => NetworkSETEX(true, ref storageApi), RespCommand.SETEXNX => NetworkSETEXNX(ref storageApi), - RespCommand.SETWITHETAG => NetworkSETWITHETAG(ref storageApi), RespCommand.SETIFMATCH => NetworkSETIFMATCH(ref storageApi), RespCommand.DEL => NetworkDEL(ref storageApi), RespCommand.RENAME => NetworkRENAME(ref storageApi), diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 353bf7bf09..4e50098c10 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -247,30 +247,23 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB case RespCommand.SETIFMATCH: case RespCommand.GETIFNOTMATCH: case RespCommand.GETWITHETAG: - // If this has an etag then we want to use it other wise null - // we know somethgin doesnt have an etag if - long etag = -1; int valueLength = value.LengthWithoutMetadata; - int desiredLength; + // always writing an array of size 2 => *2\r\n + int desiredLength = 4; ReadOnlySpan etagTruncatedVal; + // get etag to write, default etag 0 for when no etag + long etag = hasEtagInVal ? *(long*)value.ToPointer() : Constants.BaseEtag; + // remove the length of the ETAG + var etagAccountedValueLength = valueLength - payloadEtagAccountedEndOffset; if (hasEtagInVal) { - // Get value without RESP header; exclude expiration - // extract ETAG, write as long into dst, and then value - etag = *(long*)value.ToPointer(); - // remove the length of the ETAG - valueLength -= Constants.EtagSize; - // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below - etagTruncatedVal = value.AsReadOnlySpan(Constants.EtagSize); - // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n - desiredLength = 4 + 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; - } - else - { - etagTruncatedVal = value.AsReadOnlySpan(); - // instead of :(etagdigits) we will have nil after array len - desiredLength = 4 + 3 + 2 + 1 + NumUtils.NumDigits(valueLength) + 2 + valueLength + 2; + etagAccountedValueLength = valueLength - Constants.EtagSize; + payloadEtagAccountedEndOffset = Constants.EtagSize; } + // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below + etagTruncatedVal = value.AsReadOnlySpan(payloadEtagAccountedEndOffset); + // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n + desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); return; @@ -290,14 +283,14 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB } } - void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst) + void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst, bool writeDirect = false) { if (desiredLength <= dst.Length) { dst.Length = desiredLength; byte* curr = dst.SpanByte.ToPointer(); byte* end = curr + dst.SpanByte.Length; - RespWriteEtagValArray(etag, ref value, ref curr, end); + RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); return; } @@ -308,23 +301,20 @@ void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long { byte* curr = ptr; byte* end = ptr + desiredLength; - RespWriteEtagValArray(etag, ref value, ref curr, end); + RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); } } - static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end) + static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end, bool writeDirect) { // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element RespWriteUtils.WriteArrayLength(2, ref curr, end); - if (etag == -1) - { - RespWriteUtils.WriteNull(ref curr, end); - } + RespWriteUtils.WriteInteger(etag, ref curr, end); + + if (writeDirect) + RespWriteUtils.WriteDirect(value, ref curr, end); else - { - RespWriteUtils.WriteInteger(etag, ref curr, end); - } - RespWriteUtils.WriteBulkString(value, ref curr, end); + RespWriteUtils.WriteBulkString(value, ref curr, end); } bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, long newExpiry, ref SpanByte value, ref SpanByteAndMemory output) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 18cb2c9d5b..2fa1e3d70d 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -50,7 +50,8 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB recordInfo.ClearHasETag(); rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - switch (input.header.cmd) + RespCommand cmd = input.header.cmd; + switch (cmd) { case RespCommand.PFADD: var v = value.ToPointer(); @@ -74,20 +75,54 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: + bool withEtag = input.header.CheckWithEtagFlag(); + int spaceForEtag = 0; + if (withEtag) + { + spaceForEtag = Constants.EtagSize; + recordInfo.SetHasETag(); + } + // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); value.UnmarkExtraMetadata(); - value.ShrinkSerializedLength(newInputValue.Length + metadataSize); + value.ShrinkSerializedLength(newInputValue.Length + metadataSize + spaceForEtag); value.ExtraMetadata = input.arg1; - newInputValue.CopyTo(value.AsSpan()); + newInputValue.CopyTo(value.AsSpan(spaceForEtag)); + if (withEtag) + { + // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag + *(long*)value.ToPointer() = Constants.BaseEtag + 1; + if (cmd == RespCommand.SET) + { + // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX + CopyRespNumber(Constants.BaseEtag + 1, ref output); + } + + } break; case RespCommand.SETKEEPTTL: + withEtag = input.header.CheckWithEtagFlag(); + spaceForEtag = 0; + if (withEtag) + { + spaceForEtag = Constants.EtagSize; + recordInfo.SetHasETag(); + } + // Copy input to value, retain metadata in value var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - value.ShrinkSerializedLength(value.MetadataSize + setValue.Length); - setValue.CopyTo(value.AsSpan()); + value.ShrinkSerializedLength(value.MetadataSize + setValue.Length + spaceForEtag); + setValue.CopyTo(value.AsSpan(spaceForEtag)); + + if (withEtag) + { + *(long*)value.ToPointer() = Constants.BaseEtag + 1; + // Copy initial etag to output + CopyRespNumber(Constants.BaseEtag + 1, ref output); + } break; case RespCommand.SETKEEPTTLXX: @@ -176,19 +211,6 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB } CopyUpdateNumber(incrByFloat, ref value, ref output); break; - case RespCommand.SETWITHETAG: - metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - value.UnmarkExtraMetadata(); - value.ExtraMetadata = input.arg1; - recordInfo.SetHasETag(); - var valueToSet = input.parseState.GetArgSliceByRef(0); - value.ShrinkSerializedLength(value.MetadataSize + valueToSet.Length + Constants.EtagSize); - // initial etag set to 0, this is a counter based etag that is incremented on change - *(long*)value.ToPointer() = 0; - valueToSet.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); - // Copy initial etag to output - CopyRespNumber(0, ref output); - break; default: value.UnmarkExtraMetadata(); if (input.header.cmd > RespCommandExtensions.LastValidCommand) @@ -284,44 +306,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - return true; - case RespCommand.SETWITHETAG: - // SETWITHETAG WILL OVERRIDE the existing value, unless sent with RETAIN ETAG and already has etag - var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - - long etag = recordInfo.ETag && input.header.CheckRetainEtagFlag() ? oldEtag + 1 : 0; - var valueToSet = input.parseState.GetArgSliceByRef(0); - - if (value.Length < valueToSet.length + metadataSize + Constants.EtagSize) - return false; - - recordInfo.SetHasETag(); - - // Adjust value length that will result from this change - rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - value.ShrinkSerializedLength(metadataSize + valueToSet.Length + Constants.EtagSize); - rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - - if (metadataSize != 0) - value.ExtraMetadata = input.arg1; - *(long*)value.ToPointer() = etag; - valueToSet.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); - - // Copy initial etag to output - CopyRespNumber(etag, ref output); - + // Nothing is set because being in this block means NX was already violated return true; case RespCommand.SETIFMATCH: - // Cancelling the operation and returning false is used to indicate no RMW because of ETAGMISMATCH - // In this case no etag will match the "nil" etag on a record without an etag - if (!recordInfo.ETag) - { - rmwInfo.Action = RMWAction.CancelOperation; - return false; - } + long etagFromClient = input.parseState.GetLong(1); - var prevEtag = *(long*)value.ToPointer(); - var etagFromClient = input.parseState.GetLong(1); + // No Etag is the same as having an etag of 0 + long prevEtag = recordInfo.ETag ? *(long*)value.ToPointer() : 0; if (prevEtag != etagFromClient) { @@ -332,9 +323,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Need Copy update if no space for new value var inputValue = input.parseState.GetArgSliceByRef(0); - if (value.Length - Constants.EtagSize < inputValue.length) + if (value.Length < inputValue.length + Constants.EtagSize) return false; + recordInfo.SetHasETag(); // Increment the ETag long newEtag = prevEtag + 1; @@ -352,31 +344,45 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; case RespCommand.SET: case RespCommand.SETEXXX: - // If user wants to retain etag and the data has etag, we need to silently update/keep the etag, but the response should not be written with the etag + // If the user calls withetag then we need to either update an existing etag and set the value + // or set the value with an initial etag and increment it. + // If withEtag is called we return the etag back to the user + var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!input.header.CheckRetainEtagFlag()) + if (!input.header.CheckWithEtagFlag()) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record nextUpdateEtagOffset = 0; nextUpdateEtagIgnoredEnd = -1; recordInfo.ClearHasETag(); } + else if (!recordInfo.ETag) + { + // this is the case where we have withetag option and no etag from before + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; + oldEtag = Constants.BaseEtag; + } - var setValue = input.parseState.GetArgSliceByRef(0); + ArgSlice setValue = input.parseState.GetArgSliceByRef(0); // Need CU if no space for new value - metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - if (setValue.Length + metadataSize > value.Length - etagIgnoredOffset) + int metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + if (setValue.Length + metadataSize > value.Length - nextUpdateEtagOffset) return false; // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { + Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } + if (input.header.CheckWithEtagFlag()) + recordInfo.SetHasETag(); + // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); @@ -387,29 +393,54 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(nextUpdateEtagOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + if (input.header.CheckWithEtagFlag()) + { + *(long*)value.ToPointer() = oldEtag + 1; + // withetag flag means we need to write etag back to the output buffer + CopyRespNumber(oldEtag + 1, ref output); + // early return since we already updated etag + return true; + } + break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: - // respect etag retention only if input header tells you to explicitly - if (!input.header.CheckRetainEtagFlag()) + // If the user calls withetag then we need to either update an existing etag and set the value + // or set the value with an initial etag and increment it. + // If withEtag is called we return the etag back to the user + nextUpdateEtagOffset = etagIgnoredOffset; + nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + if (!input.header.CheckWithEtagFlag()) { - etagIgnoredOffset = 0; - etagIgnoredEnd = -1; + // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; recordInfo.ClearHasETag(); } + else if (!recordInfo.ETag) + { + // this is the case where we have withetag option and no etag from before + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; + oldEtag = Constants.BaseEtag; + } setValue = input.parseState.GetArgSliceByRef(0); // Need CU if no space for new value - if (setValue.Length + value.MetadataSize > value.Length - etagIgnoredOffset) + if (setValue.Length + value.MetadataSize > value.Length - nextUpdateEtagOffset) return false; // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { + Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } + if (input.header.CheckWithEtagFlag()) + recordInfo.SetHasETag(); + // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(setValue.Length + value.MetadataSize + etagIgnoredOffset); @@ -417,8 +448,17 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy input to value setValue.ReadOnlySpan.CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - // etag is not updated - return true; + + if (input.header.CheckWithEtagFlag()) + { + *(long*)value.ToPointer() = oldEtag + 1; + // withetag flag means we need to write etag back to the output buffer + CopyRespNumber(oldEtag + 1, ref output); + // early return since we already updated etag + return true; + } + + break; case RespCommand.PEXPIRE: case RespCommand.EXPIRE: @@ -711,17 +751,26 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB switch (input.header.cmd) { case RespCommand.SETIFMATCH: - if (!rmwInfo.RecordInfo.ETag) - return false; + long etagToCheckWith = input.parseState.GetLong(1); + // lack of an etag is the same as having a zero'd etag + long existingEtag; + // No Etag is the same as having an etag of 0 + if (rmwInfo.RecordInfo.ETag) + { + existingEtag = *(long*)oldValue.ToPointer(); + } + else + { + existingEtag = 0; + } - var etagToCheckWith = input.parseState.GetLong(1); - long existingEtag = *(long*)oldValue.ToPointer(); if (existingEtag != etagToCheckWith) { // cancellation and return false indicates ETag mismatch rmwInfo.Action = RMWAction.CancelOperation; return false; } + return true; case RespCommand.SETEXNX: // Expired data, return false immediately @@ -731,12 +780,14 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB rmwInfo.Action = RMWAction.ExpireAndResume; return false; } + // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } + // since this block is only hit when this an update, the NX is violated and so we can return early from it without setting the value return false; case RespCommand.SETEXXX: // Expired data, return false immediately so we do not set, since it does not exist @@ -774,8 +825,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - var cmd = input.header.cmd; - var shouldUpdateEtag = true; + RespCommand cmd = input.header.cmd; + bool shouldUpdateEtag = true; int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; long oldEtag = -1; @@ -788,89 +839,107 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte switch (cmd) { - case RespCommand.SETWITHETAG: - recordInfo.SetHasETag(); - var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); - if (metadataSize != 0) - newValue.ExtraMetadata = input.arg1; - - long etag = input.header.CheckRetainEtagFlag() && recordInfo.ETag ? oldEtag + 1 : 0; - var dest = newValue.AsSpan(Constants.EtagSize); - var src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - - Debug.Assert(src.Length + Constants.EtagSize == newValue.Length); - - src.CopyTo(dest); - - CopyRespNumber(etag, ref output); - break; - case RespCommand.SETIFMATCH: - Debug.Assert(recordInfo.ETag, "We should never be able to CU for ETag command on non-etag data."); - - // avoids double update at the end shouldUpdateEtag = false; + + // No Etag is the same as having an etag of 0 + if (!recordInfo.ETag) + { + oldEtag = 0; + } + *(long*)newValue.ToPointer() = oldEtag + 1; // Copy input to value - dest = newValue.AsSpan(Constants.EtagSize); - src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; + Span dest = newValue.AsSpan(Constants.EtagSize); + ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - Debug.Assert(src.Length + Constants.EtagSize == newValue.Length); + Debug.Assert(src.Length + Constants.EtagSize + oldValue.MetadataSize == newValue.Length); + // retain metadata + newValue.ExtraMetadata = oldValue.ExtraMetadata; src.CopyTo(dest); // Write Etag and Val back to Client - CopyRespToWithInput(ref input, ref newValue, ref output, false, 0, -1, hasEtagInVal: true); + CopyRespToWithInput(ref input, ref newValue, ref output, isFromPending: false, 0, -1, hasEtagInVal: true); break; case RespCommand.SET: case RespCommand.SETEXXX: var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!input.header.CheckRetainEtagFlag()) + + if (!input.header.CheckWithEtagFlag()) { // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record nextUpdateEtagOffset = 0; nextUpdateEtagIgnoredEnd = -1; recordInfo.ClearHasETag(); } + else if (!recordInfo.ETag) + { + // this is the case where we have withetag option and no etag from before + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; + oldEtag = 0; + recordInfo.SetHasETag(); + } // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { + Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + int metadataSize = input.arg1 == 0 ? 0 : sizeof(long); // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX - Debug.Assert(newInputValue.Length + metadataSize + etagIgnoredOffset == newValue.Length); + Debug.Assert(newInputValue.Length + metadataSize + nextUpdateEtagOffset == newValue.Length); newValue.ExtraMetadata = input.arg1; newInputValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); + + if (input.header.CheckWithEtagFlag()) + { + shouldUpdateEtag = false; + *(long*)newValue.ToPointer() = oldEtag + 1; + // withetag flag means we need to write etag back to the output buffer + CopyRespNumber(oldEtag + 1, ref output); + } + break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: nextUpdateEtagOffset = etagIgnoredOffset; nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (input.header.CheckRetainEtagFlag()) + if (!input.header.CheckWithEtagFlag()) { // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record nextUpdateEtagOffset = 0; nextUpdateEtagIgnoredEnd = -1; } + else if (!recordInfo.ETag) + { + // this is the case where we have withetag option and no etag from before + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; + oldEtag = 0; + recordInfo.SetHasETag(); + } var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - Debug.Assert(oldValue.MetadataSize + setValue.Length == newValue.Length - etagIgnoredOffset); + + Debug.Assert(oldValue.MetadataSize + setValue.Length + nextUpdateEtagOffset == newValue.Length); // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { + Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } @@ -878,6 +947,14 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Copy input to value, retain metadata of oldValue newValue.ExtraMetadata = oldValue.ExtraMetadata; setValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); + if (input.header.CheckWithEtagFlag()) + { + shouldUpdateEtag = false; + *(long*)newValue.ToPointer() = oldEtag + 1; + // withetag flag means we need to write etag back to the output buffer + CopyRespNumber(oldEtag + 1, ref output); + } + break; case RespCommand.EXPIRE: @@ -1015,6 +1092,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte return false; case RespCommand.GETEX: + shouldUpdateEtag = false; CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); if (input.arg1 > 0) diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 140c43f349..0990b3a4ab 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using Garnet.common; using Tsavorite.core; namespace Garnet.server @@ -13,77 +14,37 @@ namespace Garnet.server public readonly unsafe partial struct MainSessionFunctions : ISessionFunctions { /// - public bool SingleReader(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) - { - if (value.MetadataSize != 0 && CheckExpiry(ref value)) - return false; - - var cmd = input.header.cmd; - - var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; - - if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) - { - var existingEtag = *(long*)value.ToPointer(); - var etagToMatchAgainst = input.parseState.GetLong(0); - if (existingEtag == etagToMatchAgainst) - { - // write the value not changed message to dst, and early return - CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); - return true; - } - } - else if (cmd > RespCommandExtensions.LastValidCommand) - { - var valueLength = value.LengthWithoutMetadata; - (IMemoryOwner Memory, int Length) output = (dst.Memory, 0); - var ret = functionsState.GetCustomCommandFunctions((ushort)cmd) - .Reader(key.AsReadOnlySpan(), ref input, value.AsReadOnlySpan(), ref output, ref readInfo); - Debug.Assert(valueLength <= value.LengthWithoutMetadata); - dst.Memory = output.Memory; - dst.Length = output.Length; - return ret; - } + public bool SingleReader( + ref SpanByte key, ref RawStringInput input, + ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) => Reader(ref key, ref input, ref value, ref dst, ref readInfo, readInfo.RecordInfo.ETag); - // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - var start = 0; - var end = -1; - if (!isEtagCmd && readInfo.RecordInfo.ETag) - { - start = Constants.EtagSize; - end = value.LengthWithoutMetadata; - } - - if (cmd == RespCommand.NONE) - CopyRespTo(ref value, ref dst, start, end); - else - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, readInfo.RecordInfo.ETag); - - return true; - } /// - public bool ConcurrentReader(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo, ref RecordInfo recordInfo) + public bool ConcurrentReader( + ref SpanByte key, ref RawStringInput input, ref SpanByte value, + ref SpanByteAndMemory dst, ref ReadInfo readInfo, ref RecordInfo recordInfo) => Reader(ref key, ref input, ref value, ref dst, ref readInfo, recordInfo.ETag); + + private bool Reader(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo, bool hasEtag) { if (value.MetadataSize != 0 && CheckExpiry(ref value)) - { - // TODO: we can proactively expire if we wish, but if we do, we need to write WAL entry - // readInfo.Action = ReadAction.Expire; return false; - } var cmd = input.header.cmd; var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; - if (isEtagCmd && cmd == RespCommand.GETIFNOTMATCH) + if (cmd == RespCommand.GETIFNOTMATCH) { - var existingEtag = *(long*)value.ToPointer(); - var etagToMatchAgainst = input.parseState.GetLong(0); + long etagToMatchAgainst = input.parseState.GetLong(0); + // Any value without an etag is treated the same as a value with an etag + long existingEtag = hasEtag ? *(long*)value.ToPointer() : 0; if (existingEtag == etagToMatchAgainst) { - // write the value not changed message to dst, and early return - CopyDefaultResp(CmdStrings.RESP_VALNOTCHANGED, ref dst); + // write back array of the format [etag, nil] + var nilResp = CmdStrings.RESP_ERRNOTFOUND; + // *2\r\n: + + \r\n + + var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, writeDirect: true); return true; } } @@ -102,7 +63,7 @@ public bool ConcurrentReader(ref SpanByte key, ref RawStringInput input, ref Spa // Unless the command explicitly asks for the ETag in response, we do not write back the ETag var start = 0; var end = -1; - if (!isEtagCmd && recordInfo.ETag) + if (!isEtagCmd && hasEtag) { start = Constants.EtagSize; end = value.LengthWithoutMetadata; @@ -112,7 +73,7 @@ public bool ConcurrentReader(ref SpanByte key, ref RawStringInput input, ref Spa CopyRespTo(ref value, ref dst, start, end); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, recordInfo.ETag); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, hasEtag); } return true; diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index a8f57b343b..974817a59b 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -110,10 +110,6 @@ public int GetRMWInitialValueLength(ref RawStringInput input) ndigits = NumUtils.NumDigitsInLong(-input.arg1, ref fNeg); return sizeof(int) + ndigits + (fNeg ? 1 : 0); - case RespCommand.SETWITHETAG: - // same space as SET but with 8 additional bytes for etag at the front of the payload - newValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - return sizeof(int) + newValue.Length + Constants.EtagSize + metadataSize; case RespCommand.INCRBYFLOAT: if (!input.parseState.TryGetDouble(0, out var incrByFloat)) return sizeof(int); @@ -136,7 +132,8 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize; + int allocationForEtag = input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize + allocationForEtag; } } @@ -146,8 +143,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b if (input.header.cmd != RespCommand.NONE) { var cmd = input.header.cmd; - int etagOffset = hasEtag ? Constants.EtagSize : 0; - bool retainEtag = input.header.CheckRetainEtagFlag(); + bool withEtag = input.header.CheckWithEtagFlag(); + int etagOffset = hasEtag || withEtag ? Constants.EtagSize : 0; switch (cmd) { @@ -208,21 +205,21 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: var setValue = input.parseState.GetArgSliceByRef(0); - if (!retainEtag) + if (!withEtag) etagOffset = 0; return sizeof(int) + t.MetadataSize + setValue.Length + etagOffset; case RespCommand.SET: case RespCommand.SETEXXX: - if (!retainEtag) + if (!withEtag) etagOffset = 0; return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + etagOffset; case RespCommand.PERSIST: return sizeof(int) + t.LengthWithoutMetadata; - case RespCommand.SETWITHETAG: case RespCommand.SETIFMATCH: var newValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - return sizeof(int) + newValue.Length + Constants.EtagSize + t.MetadataSize + (input.arg1 == 0 ? 0 : sizeof(long)); + // always preserves the metadata and includes the etag + return sizeof(int) + newValue.Length + Constants.EtagSize + t.MetadataSize; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: case RespCommand.EXPIREAT: diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index ca6fd0400a..fe4ff370a8 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -554,9 +554,9 @@ public GarnetStatus DELETE(byte[] key, StoreType store return found ? GarnetStatus.OK : GarnetStatus.NOTFOUND; } - public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType) + public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool withEtag) { - return RENAME(oldKeySlice, newKeySlice, storeType, false, out _); + return RENAME(oldKeySlice, newKeySlice, storeType, false, out _, withEtag); } /// @@ -566,13 +566,15 @@ public unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, St /// The new key name. /// The type of store to perform the operation on. /// - public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, out int result) + public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, out int result, bool withEtag) { - return RENAME(oldKeySlice, newKeySlice, storeType, true, out result); + return RENAME(oldKeySlice, newKeySlice, storeType, true, out result, withEtag); } - private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool isNX, out int result) + + // HK TODO + private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool isNX, out int result, bool withEtag) { - RawStringInput input = new RawStringInput(RespCommand.GETWITHETAG); + RawStringInput input = default; var returnStatus = GarnetStatus.NOTFOUND; result = -1; @@ -610,20 +612,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S Debug.Assert(!o.IsSpanByte); var memoryHandle = o.Memory.Memory.Pin(); var ptrVal = (byte*)memoryHandle.Pointer; - var end = ptrVal + o.Length; - - // read array metadata - RespReadUtils.ReadSignedArrayLength(out int numItemsInArr, ref ptrVal, end); - - Debug.Assert(numItemsInArr == 2, "Items in GETWITHETAG response array should always be 2"); - - bool oldKeyHadEtag = !RespReadUtils.ReadNil(ref ptrVal, end, out var _); - if (oldKeyHadEtag) - { - RespReadUtils.Read64Int(out long _, ref ptrVal, end); - } - // read length of val RespReadUtils.ReadUnsignedLengthHeader(out var headerLength, ref ptrVal, ptrVal + o.Length); // Find expiration time of the old key @@ -638,159 +627,63 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S var expirePtrVal = (byte*)expireMemoryHandle.Pointer; RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _); - input = new RawStringInput(RespCommand.SETEXNX); + input = isNX ? new RawStringInput(RespCommand.SETEXNX): new RawStringInput(RespCommand.SET); // If the key has an expiration, set the new key with the expiration if (expireTimeMs > 0) { - if (isNX && !oldKeyHadEtag) + if (!withEtag && !isNX) + { + SETEX(newKeySlice, newValSlice, TimeSpan.FromMilliseconds(expireTimeMs), ref context); + } + else { + // Move payload forward to make space for RespInputHeader and Metadata parseState.InitializeWithArgument(newValSlice); input.parseState = parseState; input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; + if (withEtag) + input.header.CheckWithEtagFlag(); + var setStatus = SET_Conditional(ref newKey, ref input, ref context); - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - returnStatus = GarnetStatus.OK; - } - else - { - if (!isNX && !oldKeyHadEtag) + if (isNX) { - SETEX(newKeySlice, newValSlice, TimeSpan.FromMilliseconds(expireTimeMs), ref context); - goto POSTINSERTIONS; - } - /* - This block here on handles combinations: (IsNX && oldKeyHadEtag and !isNX && oldKEyHadEtag) - regardless of whether or not it isNX we know oldKeyHadEtag is true, so we must always use SETWITHETAG. - IsNx just conditionally dispatches SETWITHETAG on newkey after making sure newkey didnt already exist - */ - - // we need to check if old key exists, and if it exists does it have an etag - input.header.cmd = RespCommand.GETWITHETAG; - var getNewKeyWithEtagOutput = new SpanByteAndMemory(); - GarnetStatus getNewKeyWithEtagStatus = GET(ref newKey, ref input, ref getNewKeyWithEtagOutput, ref context); - - // if it exists and the command is for isNX we need to early exit - if (isNX && getNewKeyWithEtagStatus == GarnetStatus.OK) - { - result = 0; + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; returnStatus = GarnetStatus.OK; - getNewKeyWithEtagOutput.Memory.Dispose(); - goto POSTINSERTIONS; } - - // we know this isNX is false inside of the following block, here we need to check if it had an etag - if (getNewKeyWithEtagStatus == GarnetStatus.OK) - { - // if the new key has an etag we want to retain it - using (MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin()) - { - byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; - byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; - - // skip past the array metadata - RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); - - // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set - if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) - { - input.header.SetRetainEtagFlag(); - } - } - - getNewKeyWithEtagOutput.Memory.Dispose(); - } - - // SETWITHETAG newkey valueFromOldkey with expiration - // since we moved from an oldkey to newkey we reset the etag on the new key - parseState.InitializeWithArgument(newValSlice); - input.header.cmd = RespCommand.SETWITHETAG; - input.parseState = parseState; - input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; - - var tempOutput = new SpanByteAndMemory(); - SET_Conditional(ref newKey, ref input, ref tempOutput, ref context, RespCommand.SETWITHETAG); - returnStatus = GarnetStatus.OK; - - result = isNX ? 1 : 0; } } else if (expireTimeMs == -1) // Its possible to have expireTimeMs as 0 (Key expired or will be expired now) or -2 (Key does not exist), in those cases we don't SET the new key { - if (isNX && !oldKeyHadEtag) + if (!withEtag && !isNX) + { + var value = newValSlice.SpanByte; + SET(ref newKey, ref value, ref context); + } + else { // Build parse state parseState.InitializeWithArgument(newValSlice); input.parseState = parseState; - var setStatus = SET_Conditional(ref newKey, ref input, ref context); + if (withEtag) + input.header.SetWithEtagFlag(); - // For SET NX `NOTFOUND` means the operation succeeded - result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; - returnStatus = GarnetStatus.OK; - } - else - { - if (!isNX && !oldKeyHadEtag) - { - var value = SpanByte.FromPinnedPointer(ptrVal, headerLength); - SET(ref newKey, ref value, ref context); - goto POSTINSERTIONS; - } - - // we need to check if old key exists, and if it doesnt exist we can proceed with setting new key and expiry - input.header.cmd = RespCommand.GETWITHETAG; - var getNewKeyWithEtagOutput = new SpanByteAndMemory(); - GarnetStatus getNewKeyWithEtagStatus = GET(ref newKey, ref input, ref getNewKeyWithEtagOutput, ref context); + var setStatus = SET_Conditional(ref newKey, ref input, ref context); - // if it exists and the command is for isNX we need to early exit - if (isNX && getNewKeyWithEtagStatus == GarnetStatus.OK) + if (isNX) { - result = 0; + // For SET NX `NOTFOUND` means the operation succeeded + result = setStatus == GarnetStatus.NOTFOUND ? 1 : 0; returnStatus = GarnetStatus.OK; - goto POSTINSERTIONS; - } - - // we know this isNX is false inside of the following block, here we need to check if it had an etag - if (getNewKeyWithEtagStatus == GarnetStatus.OK) - { - // if the new key has an etag we want to retain it - using (MemoryHandle getWithNewKeyWithEtagMemoryHandle = getNewKeyWithEtagOutput.Memory.Memory.Pin()) - { - byte* getWithNewKeyWithEtagPtr = (byte*)getWithNewKeyWithEtagMemoryHandle.Pointer; - byte* endOfOutputBuffer = getWithNewKeyWithEtagPtr + getNewKeyWithEtagOutput.Length; - - // skip past the array metadata - RespReadUtils.ReadSignedArrayLength(out int numItemsInNewKeyArr, ref getWithNewKeyWithEtagPtr, endOfOutputBuffer); - - // the next item is meant to be etag, if it is not nil we know we need to retain the etag on it in our next set - if (!RespReadUtils.ReadNil(ref getWithNewKeyWithEtagPtr, endOfOutputBuffer, out var _)) - { - input.header.SetRetainEtagFlag(); - } - } - - getNewKeyWithEtagOutput.Memory.Dispose(); } - - // SETWITHETAG newkey valueFromOldkey with expiration - // since we moved from an oldkey to newkey we reset the etag on the new key - parseState.InitializeWithArgument(newValSlice); - input.header.cmd = RespCommand.SETWITHETAG; - input.parseState = parseState; - - var tempOutput = new SpanByteAndMemory(); - SET_Conditional(ref newKey, ref input, ref tempOutput, ref context, RespCommand.SETWITHETAG); - returnStatus = GarnetStatus.OK; - - result = isNX ? 1 : 0; } } - POSTINSERTIONS: + expireSpan.Memory.Dispose(); memoryHandle.Dispose(); o.Memory.Dispose(); diff --git a/libs/server/Transaction/TxnKeyManager.cs b/libs/server/Transaction/TxnKeyManager.cs index 2b569d9730..7238a9ecb6 100644 --- a/libs/server/Transaction/TxnKeyManager.cs +++ b/libs/server/Transaction/TxnKeyManager.cs @@ -143,7 +143,6 @@ internal int GetKeys(RespCommand command, int inputCount, out ReadOnlySpan RespCommand.GETIFNOTMATCH => SingleKey(1, false, LockType.Shared), RespCommand.GETWITHETAG => SingleKey(1, false, LockType.Shared), RespCommand.SET => SingleKey(1, false, LockType.Exclusive), - RespCommand.SETWITHETAG => SingleKey(1, false, LockType.Exclusive), RespCommand.SETIFMATCH => SingleKey(1, false, LockType.Exclusive), RespCommand.GETRANGE => SingleKey(1, false, LockType.Shared), RespCommand.SETRANGE => SingleKey(1, false, LockType.Exclusive), diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 52facba875..516fd478c6 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -240,7 +240,6 @@ public class SupportedCommand new("SETIFMATCH", RespCommand.SETIFMATCH), new("SETNX", RespCommand.SETNX), new("SETRANGE", RespCommand.SETRANGE), - new("SETWITHETAG", RespCommand.SETWITHETAG), new("SISMEMBER", RespCommand.SISMEMBER), new("SLAVEOF", RespCommand.SECONDARYOF), new("SMEMBERS", RespCommand.SMEMBERS), diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 6c8558ddd3..8a513ea4fb 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -5027,21 +5027,6 @@ static async Task DoSetKeepTtlXxAsync(GarnetClient client) } } - [Test] - public async Task SetWithEtagACLsAsync() - { - await CheckCommandsAsync( - "SETWITHETAG", - [DoSetWithEtagAsync] - ); - - static async Task DoSetWithEtagAsync(GarnetClient client) - { - long val = await client.ExecuteForLongResultAsync("SETWITHETAG", ["foo", "bar"]); - ClassicAssert.AreEqual(0, val); - } - } - [Test] public async Task SetIfMatchACLsAsync() { diff --git a/test/Garnet.test/RespAofTests.cs b/test/Garnet.test/RespAofTests.cs index 98d8199342..5c2842d1f5 100644 --- a/test/Garnet.test/RespAofTests.cs +++ b/test/Garnet.test/RespAofTests.cs @@ -229,7 +229,7 @@ public void AofRMWStoreRecoverTest() var db = redis.GetDatabase(0); db.StringSet("SeAofUpsertRecoverTestKey1", "SeAofUpsertRecoverTestValue1", expiry: TimeSpan.FromDays(1), when: When.NotExists); db.StringSet("SeAofUpsertRecoverTestKey2", "SeAofUpsertRecoverTestValue2", expiry: TimeSpan.FromDays(1), when: When.NotExists); - db.Execute("SETWITHETAG", "SeAofUpsertRecoverTestKey3", "SeAofUpsertRecoverTestValue3"); + db.Execute("SET", "SeAofUpsertRecoverTestKey3", "SeAofUpsertRecoverTestValue3", "WITHETAG"); db.Execute("SETIFMATCH", "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", "0"); } diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index 487ce23141..5b05949e3d 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -202,7 +202,7 @@ public override unsafe void Main(TGarnetApi garnetApi, ref CustomPro RawStringInput input = new RawStringInput(RespCommand.SET); input.header.cmd = RespCommand.SET; // if we send a SET we must explictly ask it to retain etag, and use conditional set - input.header.SetRetainEtagFlag(); + input.header.SetWithEtagFlag(); fixed (byte* valuePtr = valueToMessWith.ToArray()) { @@ -1376,8 +1376,8 @@ public void CustomTxnEtagInteractionTest() try { - db.Execute("SETWITHETAG", key1, value1); - db.Execute("SETWITHETAG", key2, value2); + db.Execute("SET", key1, value1, "WITHETAG"); + db.Execute("SET", key2, value2, "WITHETAG"); RedisResult result = db.Execute("RANDOPS", key1, key2); @@ -1414,8 +1414,8 @@ public void CustomProcEtagInteractionTest() try { - db.Execute("SETWITHETAG", key1, value1); - db.Execute("SETWITHETAG", key2, value2); + db.Execute("SET", key1, value1, "WITHETAG"); + db.Execute("SET", key2, value2, "WITHETAG"); // incr key2, and just get key1 RedisResult result = db.Execute("INCRGET", key2, key1); diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index c5ba6a9d5d..31deb2485f 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -38,13 +38,13 @@ public void TearDown() #region ETAG SET Happy Paths [Test] - public void SetWithEtagReturnsEtagForNewData() + public void SETReturnsEtagForNewData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + RedisResult res = db.Execute("SET", ["rizz", "buzz", "WITHETAG"]); long etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); } [Test] @@ -54,9 +54,9 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() IDatabase db = redis.GetDatabase(0); var key = "florida"; - RedisResult res = (RedisResult)db.Execute("SETWITHETAG", [key, "one"]); + RedisResult res = (RedisResult)db.Execute("SET", [key, "one", "WITHETAG"]); long initalEtag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, initalEtag); + ClassicAssert.AreEqual(1, initalEtag); // ETAGMISMATCH test var incorrectEtag = 1738; @@ -69,7 +69,7 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() long nextEtag = long.Parse(setIfMatchRes[0].ToString()); string value = setIfMatchRes[1].ToString(); - ClassicAssert.AreEqual(1, nextEtag); + ClassicAssert.AreEqual(2, nextEtag); ClassicAssert.AreEqual(value, "nextone"); // set a bigger val @@ -77,7 +77,7 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() nextEtag = long.Parse(setIfMatchRes[0].ToString()); value = setIfMatchRes[1].ToString(); - ClassicAssert.AreEqual(2, nextEtag); + ClassicAssert.AreEqual(3, nextEtag); ClassicAssert.AreEqual(value, "nextnextone"); // ETAGMISMATCH again @@ -89,7 +89,7 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() nextEtag = long.Parse(setIfMatchRes[0].ToString()); value = setIfMatchRes[1].ToString(); - ClassicAssert.AreEqual(3, nextEtag); + ClassicAssert.AreEqual(4, nextEtag); ClassicAssert.AreEqual(value, "lastOne"); } @@ -109,14 +109,14 @@ public void GetWithEtagReturnsValAndEtagForKey() ClassicAssert.IsTrue(nonExistingData.IsNull); // insert data - var initEtag = db.Execute("SETWITHETAG", [key, "hkhalid"]); - ClassicAssert.AreEqual(0, long.Parse(initEtag.ToString())); + var initEtag = db.Execute("SET", key, "hkhalid", "WITHETAG"); + ClassicAssert.AreEqual(1, long.Parse(initEtag.ToString())); RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", [key]); long etag = long.Parse(res[0].ToString()); string value = res[1].ToString(); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); ClassicAssert.AreEqual("hkhalid", value); } @@ -132,66 +132,133 @@ public void GetIfNotMatchReturnsDataWhenEtagDoesNotMatch() ClassicAssert.IsTrue(nonExistingData.IsNull); // insert data - var _ = db.Execute("SETWITHETAG", [key, "maximus"]); + var _ = db.Execute("SET", key, "maximus", "WITHETAG"); - RedisResult noDataOnMatch = db.Execute("GETIFNOTMATCH", [key, 0]); + RedisResult[] noDataOnMatch = (RedisResult[])db.Execute("GETIFNOTMATCH", key, 1); + ClassicAssert.AreEqual("1", noDataOnMatch[0].ToString()); + ClassicAssert.IsTrue(noDataOnMatch[1].IsNull); - ClassicAssert.AreEqual("NOTCHANGED", noDataOnMatch.ToString()); - - RedisResult[] res = (RedisResult[])db.Execute("GETIFNOTMATCH", [key, 1]); + RedisResult[] res = (RedisResult[])db.Execute("GETIFNOTMATCH", [key, 2]); long etag = long.Parse(res[0].ToString()); string value = res[1].ToString(); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); ClassicAssert.AreEqual("maximus", value); } + [Test] + public void SetWithEtagWorksWithMetadata() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + // Scenario: set withetag with expiration on non existing key + var key1 = "key1"; + var res1 = db.Execute("SET", key1, "value1", "WITHETAG", "EX", 10); + long etag1 = (long)res1; + ClassicAssert.AreEqual(1, etag1); + db.KeyDelete(key1); // Cleanup + + // Scenario: set with etag with expiration NX with existing key + var key2 = "key2"; + db.Execute("SET", key2, "value2", "WITHETAG"); + var res2 = db.Execute("SET", key2, "value3", "WITHETAG", "NX", "EX", 10); + // it 1 if key set, 0 if not set + ClassicAssert.AreEqual(1, (int)res2); + db.KeyDelete(key2); // Cleanup + + // Scenario: set with etag with expiration NX with non-existent key + var key3 = "key3"; + var res3 = db.Execute("SET", key3, "value4", "WITHETAG", "NX", "EX", 10); + long etag3 = (long)res3; + ClassicAssert.AreEqual(1, etag3); + db.KeyDelete(key3); // Cleanup + + // Scenario: set with etag with expiration XX + var key4 = "key4"; + db.Execute("SET", key4, "value5", "WITHETAG"); + var res4 = db.Execute("SET", key4, "value6", "WITHETAG", "XX", "EX", 10); + long etag4 = (long)res4; + ClassicAssert.AreEqual(2, etag4); + db.KeyDelete(key4); // Cleanup + + // Scenario: set with etag with expiration on existing data with etag + var key5 = "key5"; + db.Execute("SET", key5, "value7", "WITHETAG"); + var res5 = db.Execute("SET", key5, "value8", "WITHETAG", "EX", 10); + long etag5 = (long)res5; + ClassicAssert.AreEqual(2, etag5); + db.KeyDelete(key5); // Cleanup + + // Scenario: set with etag with expiration on existing data without etag + var key6 = "key6"; + db.Execute("SET", key6, "value9"); + var res6 = db.Execute("SET", key6, "value10", "WITHETAG", "EX", 10); + long etag6 = (long)res6; + ClassicAssert.AreEqual(1, etag6); + db.KeyDelete(key6); // Cleanup + + // Scenario: set with keepttl on key with etag and expiration should retain metadata and + var key7 = "key7"; + db.Execute("SET", key7, "value11", "WITHETAG", "EX", 10); + var res7 = db.Execute("SET", key7, "value12", "WITHETAG", "KEEPTTL"); + long etag7 = (long)res7; + ClassicAssert.AreEqual(2, etag7); + } + #endregion # region Edgecases [Test] - public void SetWithEtagOnAlreadyExistingSetWithEtagDataOverridesItWithInitialEtag() + public void SETOnAlreadyExistingSETDataOverridesItWithInitialEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + RedisResult res = db.Execute("SET", "rizz", "buzz", "WITHETAG"); long etag = (long)res; - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); // update to value to update the etag RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); etag = (long)updateRes[0]; - ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual(2, etag); ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); // inplace update - res = db.Execute("SETWITHETAG", ["rizz", "meow"]); + res = db.Execute("SET", "rizz", "meow", "WITHETAG"); etag = (long)res; - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(3, etag); // update to value to update the etag updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); etag = (long)updateRes[0]; - ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual(4, etag); ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "oneofus"]); + res = db.Execute("SET", ["rizz", "oneofus", "WITHETAG"]); etag = (long)res; - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(5, etag); + + // now we should do a getwithetag and see the etag as 0 + res = db.Execute("SET", ["rizz", "oneofus"]); + ClassicAssert.AreEqual(res.ToString(), "OK"); + + var getwithetagRes = (RedisResult[])db.Execute("GETWITHETAG", "rizz"); + ClassicAssert.AreEqual("0", getwithetagRes[0].ToString()); } [Test] - public void SetWithEtagWithRetainEtagOnAlreadyExistingSetWithEtagDataOverridesItButUpdatesEtag() + public void SETWithWITHETAGOnAlreadyExistingSETDataOverridesItButUpdatesEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz", "RETAINETAG"]); + RedisResult res = db.Execute("SET", ["rizz", "buzz", "WITHETAG"]); long etag = (long)res; - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); // update to value to update the etag RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); @@ -200,7 +267,7 @@ public void SetWithEtagWithRetainEtagOnAlreadyExistingSetWithEtagDataOverridesIt ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); // inplace update - res = db.Execute("SETWITHETAG", ["rizz", "meow", "RETAINETAG"]); + res = db.Execute("SET", ["rizz", "meow", "WITHETAG"]); etag = (long)res; ClassicAssert.AreEqual(2, etag); @@ -211,13 +278,13 @@ public void SetWithEtagWithRetainEtagOnAlreadyExistingSetWithEtagDataOverridesIt ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "oneofus", "RETAINETAG"]); + res = db.Execute("SET", ["rizz", "oneofus", "WITHETAG"]); etag = (long)res; ClassicAssert.AreEqual(4, etag); } [Test] - public void SetWithEtagWithRetainEtagOnAlreadyExistingNonEtagDataOverridesItToInitialEtag() + public void SETWithWITHETAGOnAlreadyExistingNonEtagDataOverridesItToInitialEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); @@ -225,18 +292,18 @@ public void SetWithEtagWithRetainEtagOnAlreadyExistingNonEtagDataOverridesItToIn ClassicAssert.IsTrue(db.StringSet("rizz", "used")); // inplace update - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz", "RETAINETAG"]); + RedisResult res = db.Execute("SET", ["rizz", "buzz", "WITHETAG"]); long etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); db.KeyDelete("rizz"); ClassicAssert.IsTrue(db.StringSet("rizz", "my")); // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "some", "RETAINETAG"]); + res = db.Execute("SET", ["rizz", "some", "WITHETAG"]); etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); } #endregion @@ -244,7 +311,7 @@ public void SetWithEtagWithRetainEtagOnAlreadyExistingNonEtagDataOverridesItToIn #region ETAG Apis with non-etag data [Test] - public void SetWithEtagOnAlreadyExistingNonEtagDataOverridesIt() + public void SETOnAlreadyExistingNonEtagDataOverridesIt() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); @@ -252,41 +319,22 @@ public void SetWithEtagOnAlreadyExistingNonEtagDataOverridesIt() ClassicAssert.IsTrue(db.StringSet("rizz", "used")); // inplace update - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); + RedisResult res = db.Execute("SET", ["rizz", "buzz", "WITHETAG"]); long etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); - - db.KeyDelete("rizz"); - - ClassicAssert.IsTrue(db.StringSet("rizz", "my")); + ClassicAssert.AreEqual(1, etag); - // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "some"]); + res = db.Execute("SET", ["rizz", "buzz", "WITHETAG"]); etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); - } - - [Test] - public void SetWithEtagWithRetainEtagOnAlreadyExistingNonEtagDataOverridesIt() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - IDatabase db = redis.GetDatabase(0); - - ClassicAssert.IsTrue(db.StringSet("rizz", "used")); - - // inplace update - RedisResult res = db.Execute("SETWITHETAG", ["rizz", "buzz"]); - long etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(2, etag); db.KeyDelete("rizz"); ClassicAssert.IsTrue(db.StringSet("rizz", "my")); // Copy update - res = db.Execute("SETWITHETAG", ["rizz", "some"]); + res = db.Execute("SET", ["rizz", "some", "WITHETAG"]); etag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.AreEqual(1, etag); } @@ -298,7 +346,7 @@ public void SetIfMatchOnNonEtagDataReturnsEtagMismatch() var _ = db.StringSet("h", "k"); - var res = db.Execute("SETIFMATCH", ["h", "t", "0"]); + var res = db.Execute("SETIFMATCH", ["h", "t", "2"]); ClassicAssert.AreEqual("ETAGMISMATCH", res.ToString()); } @@ -312,12 +360,12 @@ public void GetIfNotMatchOnNonEtagDataReturnsNilForEtagAndCorrectData() var res = (RedisResult[])db.Execute("GETIFNOTMATCH", ["h", "1"]); - ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual("0", res[0].ToString()); ClassicAssert.AreEqual("k", res[1].ToString()); } [Test] - public void GetWithEtagOnNonEtagDataReturnsNilForEtagAndCorrectData() + public void GetWithEtagOnNonEtagDataReturns0ForEtagAndCorrectData() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); @@ -325,7 +373,7 @@ public void GetWithEtagOnNonEtagDataReturnsNilForEtagAndCorrectData() var _ = db.StringSet("h", "k"); var res = (RedisResult[])db.Execute("GETWITHETAG", ["h"]); - ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual("0", res[0].ToString()); ClassicAssert.AreEqual("k", res[1].ToString()); } @@ -340,7 +388,7 @@ public void SingleEtagSetGet() var db = redis.GetDatabase(0); string origValue = "abcdefg"; - db.Execute("SETWITHETAG", ["mykey", origValue]); + db.Execute("SET", ["mykey", origValue, "WITHETAG"]); string retValue = db.StringGet("mykey"); @@ -354,7 +402,7 @@ public async Task SingleUnicodeEtagSetGetGarnetClient() db.Connect(); string origValue = "笑い男"; - await db.ExecuteForLongResultAsync("SETWITHETAG", ["mykey", origValue]); + await db.ExecuteForLongResultAsync("SET", ["mykey", origValue, "WITHETAG"]); string retValue = await db.StringGetAsync("mykey"); @@ -373,9 +421,9 @@ public async Task LargeEtagSetGet() for (int i = 0; i < length; i++) value[i] = (byte)((byte)'a' + ((byte)i % 26)); - RedisResult res = await db.ExecuteAsync("SETWITHETAG", ["mykey", value]); + RedisResult res = await db.ExecuteAsync("SET", ["mykey", value, "WITHETAG"]); long initalEtag = long.Parse(res.ToString()); - ClassicAssert.AreEqual(0, initalEtag); + ClassicAssert.AreEqual(1, initalEtag); // Backwards compatability of data set with etag and plain GET call var retvalue = (byte[])await db.StringGetAsync("mykey"); @@ -392,13 +440,8 @@ public void SetExpiryForEtagSetData() string origValue = "abcdefghij"; // set with etag - long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", origValue]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); - - // Expire the key in few seconds from now - ClassicAssert.IsTrue( - db.KeyExpire("mykey", TimeSpan.FromSeconds(2)) - ); + long initalEtag = long.Parse(db.Execute("SET", ["mykey", origValue, "EX", 2, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, initalEtag); string retValue = db.StringGet("mykey"); ClassicAssert.AreEqual(origValue, retValue, "Get() before expiration"); @@ -435,11 +478,8 @@ public void SetExpiryHighPrecisionForEtagSetDatat() var origValue = "abcdeghijklmno"; // set with etag - long initalEtag = long.Parse(db.Execute("SETWITHETAG", ["mykey", origValue]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); - - // Expire the key in few seconds from now - db.KeyExpire("mykey", TimeSpan.FromSeconds(1.9)); + long initalEtag = long.Parse(db.Execute("SET", ["mykey", origValue, "PX", 1900, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, initalEtag); string retValue = db.StringGet("mykey"); ClassicAssert.AreEqual(origValue, retValue); @@ -453,103 +493,6 @@ public void SetExpiryHighPrecisionForEtagSetDatat() ClassicAssert.AreEqual(null, retValue); } - [Test] - public void SetGetWithRetainEtagForEtagSetData() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string key = "mykey"; - string origValue = "abcdefghijklmnopqrst"; - - // Initial set - var _ = db.Execute("SETWITHETAG", [key, origValue]); - string retValue = db.StringGet(key); - ClassicAssert.AreEqual(origValue, retValue); - - // Smaller new value without expiration - string newValue1 = "abcdefghijklmnopqrs"; - - retValue = db.Execute("SET", [key, newValue1, "GET", "RETAINETAG"]).ToString(); - - ClassicAssert.AreEqual(origValue, retValue); - - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue1, retValue); - - // This should increase the ETAG internally so we have a check for that here - long checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); - ClassicAssert.AreEqual(1, checkEtag); - - // Smaller new value with KeepTtl - string newValue2 = "abcdefghijklmnopqr"; - retValue = db.Execute("SET", [key, newValue2, "GET", "RETAINETAG"]).ToString(); - - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); - ClassicAssert.AreEqual(2, checkEtag); - - ClassicAssert.AreEqual(newValue1, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue2, retValue); - var expiry = db.KeyTimeToLive(key); - ClassicAssert.IsNull(expiry); - - // Smaller new value with expiration - string newValue3 = "01234"; - retValue = db.Execute("SET", [key, newValue3, "EX", "10", "GET", "RETAINETAG"]).ToString(); - ClassicAssert.AreEqual(newValue2, retValue); - - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); - ClassicAssert.AreEqual(3, checkEtag); - - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue3, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); - - // Larger new value with expiration - string newValue4 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - retValue = db.Execute("SET", [key, newValue4, "EX", "100", "GET", "RETAINETAG"]).ToString(); - ClassicAssert.AreEqual(newValue3, retValue); - - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); - ClassicAssert.AreEqual(4, checkEtag); - - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue4, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsTrue(expiry.Value.TotalSeconds > 0); - - // Smaller new value without expiration - string newValue5 = "0123401234"; - retValue = db.Execute("SET", [key, newValue5, "GET", "RETAINETAG"]).ToString(); - ClassicAssert.AreEqual(newValue4, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue5, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsNull(expiry); - - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); - ClassicAssert.AreEqual(5, checkEtag); - - // Larger new value without expiration - string newValue6 = "abcdefghijklmnopqrstabcdefghijklmnopqrst"; - retValue = db.Execute("SET", [key, newValue6, "GET", "RETAINETAG"]).ToString(); - ClassicAssert.AreEqual(newValue5, retValue); - retValue = db.StringGet(key); - ClassicAssert.AreEqual(newValue6, retValue); - expiry = db.KeyTimeToLive(key); - ClassicAssert.IsNull(expiry); - - // This should increase the ETAG internally so we have a check for that here - checkEtag = long.Parse(db.Execute("GETWITHETAG", [key])[0].ToString()); - ClassicAssert.AreEqual(6, checkEtag); - } - [Test] public void SetExpiryIncrForEtagSetData() { @@ -559,7 +502,7 @@ public void SetExpiryIncrForEtagSetData() // Key storing integer var nVal = -100000; var strKey = "key1"; - db.Execute("SETWITHETAG", [strKey, nVal]); + db.Execute("SET", [strKey, nVal, "WITHETAG"]); db.KeyExpire(strKey, TimeSpan.FromSeconds(5)); string res1 = db.StringGet(strKey); @@ -568,7 +511,7 @@ public void SetExpiryIncrForEtagSetData() // This should increase the ETAG internally so we have a check for that here var checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(1, checkEtag); + ClassicAssert.AreEqual(2, checkEtag); string res = db.StringGet(strKey); long nRetVal = Convert.ToInt64(res); @@ -589,8 +532,7 @@ public void SetExpiryIncrForEtagSetData() Thread.Sleep(5000); - // Expired key, restart increment,after exp this is treated as new record - // without etag + // Expired key, restart increment,after exp this is treated as new record n = db.StringIncrement(strKey); ClassicAssert.AreEqual(1, n); @@ -599,7 +541,8 @@ public void SetExpiryIncrForEtagSetData() ClassicAssert.AreEqual(1, nRetVal); var etagGet = (RedisResult[])db.Execute("GETWITHETAG", [strKey]); - ClassicAssert.IsTrue(etagGet[0].IsNull); + // Etag will show up as 0 since the previous one had expired + ClassicAssert.AreEqual("0", etagGet[0].ToString()); ClassicAssert.AreEqual(1, Convert.ToInt64(etagGet[1])); } @@ -611,10 +554,10 @@ public void IncrDecrChangeDigitsWithExpiry() var strKey = "key1"; - db.Execute("SETWITHETAG", [strKey, 9]); + db.Execute("SET", [strKey, 9, "WITHETAG"]); long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(0, checkEtag); + ClassicAssert.AreEqual(1, checkEtag); db.KeyExpire(strKey, TimeSpan.FromSeconds(5)); @@ -647,16 +590,16 @@ public void StringSetOnAnExistingEtagDataOverrides() var db = redis.GetDatabase(0); var strKey = "mykey"; - db.Execute("SETWITHETAG", [strKey, 9]); + db.Execute("SET", [strKey, 9, "WITHETAG"]); long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(0, checkEtag); + ClassicAssert.AreEqual(1, checkEtag); - // Unless the SET was called with RETAINETAG a call to set will override the setwithetag to a new + // Unless the SET was called with WITHETAG a call to set will override the SET to a new // value altogether, this will make it lose it's etag capability. This is a limitation for Etags // because plain sets are upserts (blind updates), and currently we cannot increase the latency in // the common path for set to check beyong Readonly address for the existence of a record with ETag. - // This means that sets are complete upserts and clients need to use setifmatch, or set with RETAINETAG + // This means that sets are complete upserts and clients need to use setifmatch, or set with WITHETAG // if they want each consequent set to maintain the key value pair's etag property. ClassicAssert.IsTrue(db.StringSet(strKey, "ciaociao")); @@ -664,7 +607,7 @@ public void StringSetOnAnExistingEtagDataOverrides() ClassicAssert.AreEqual("ciaociao", retVal); var res = (RedisResult[])db.Execute("GETWITHETAG", [strKey]); - ClassicAssert.IsTrue(res[0].IsNull); + ClassicAssert.AreEqual("0", res[0].ToString()); ClassicAssert.AreEqual("ciaociao", res[1].ToString()); } @@ -675,13 +618,13 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() var db = redis.GetDatabase(0); var strKey = "mykey"; - db.Execute("SETWITHETAG", [strKey, 9]); + db.Execute("SET", strKey, "9", "WITHETAG"); long checkEtag = (long)db.Execute("GETWITHETAG", [strKey])[0]; - ClassicAssert.AreEqual(0, checkEtag); + ClassicAssert.AreEqual(1, checkEtag); - // Unless you explicitly call SET with RETAINETAG option you will lose the etag on the previous key-value pair - db.Execute("SET", [strKey, "ciaociao", "RETAINETAG"]); + // Unless you explicitly call SET with WITHETAG option you will lose the etag on the previous key-value pair + db.Execute("SET", [strKey, "ciaociao", "WITHETAG"]); string retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociao", retVal); @@ -690,7 +633,7 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() ClassicAssert.AreEqual(1, (long)res[0]); // on subsequent upserts we are still increasing the etag transparently - db.Execute("SET", [strKey, "ciaociaociao", "RETAINETAG"]); + db.Execute("SET", [strKey, "ciaociaociao", "WITHETAG"]); retVal = db.StringGet(strKey).ToString(); ClassicAssert.AreEqual("ciaociaociao", retVal); @@ -701,7 +644,7 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() } [Test] - public void LockTakeReleaseOnAValueInitiallySetWithEtag() + public void LockTakeReleaseOnAValueInitiallySET() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -709,8 +652,8 @@ public void LockTakeReleaseOnAValueInitiallySetWithEtag() string key = "lock-key"; string value = "lock-value"; - var initalEtag = long.Parse(db.Execute("SETWITHETAG", [key, value]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); + var initalEtag = long.Parse(db.Execute("SET", [key, value, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, initalEtag); var success = db.LockTake(key, value, TimeSpan.FromSeconds(100)); ClassicAssert.IsFalse(success); @@ -748,8 +691,8 @@ public void SingleDecrForEtagSetData(string strKey, int nVal) var db = redis.GetDatabase(0); // Key storing integer - var initalEtag = long.Parse(db.Execute("SETWITHETAG", [strKey, nVal]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); + var initalEtag = long.Parse(db.Execute("SET", [strKey, nVal, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, initalEtag); long n = db.StringDecrement(strKey); ClassicAssert.AreEqual(nVal - 1, n); @@ -771,8 +714,8 @@ public void SingleDecrByForEtagSetData(long nVal, long nDecr) var db = redis.GetDatabase(0); // Key storing integer val var strKey = "key1"; - var initalEtag = long.Parse(db.Execute("SETWITHETAG", [strKey, nVal]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); + var initalEtag = long.Parse(db.Execute("SET", [strKey, nVal, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, initalEtag); long n = db.StringDecrement(strKey, nDecr); @@ -798,8 +741,8 @@ public void SimpleIncrementInvalidValueForEtagSetdata(RespCommand cmd) { var key = $"key{i}"; var exception = false; - var initalEtag = long.Parse(db.Execute("SETWITHETAG", [key, values[i]]).ToString()); - ClassicAssert.AreEqual(0, initalEtag); + var initalEtag = long.Parse(db.Execute("SET", [key, values[i], "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, initalEtag); try { @@ -840,19 +783,19 @@ public void SimpleIncrementOverflowForEtagSetData(RespCommand cmd) switch (cmd) { case RespCommand.INCR: - _ = db.Execute("SETWITHETAG", [key, long.MaxValue.ToString()]); + _ = db.Execute("SET", [key, long.MaxValue.ToString(), "WITHETAG"]); _ = db.StringIncrement(key); break; case RespCommand.DECR: - _ = db.Execute("SETWITHETAG", [key, long.MinValue.ToString()]); + _ = db.Execute("SET", [key, long.MinValue.ToString(), "WITHETAG"]); _ = db.StringDecrement(key); break; case RespCommand.INCRBY: - _ = db.Execute("SETWITHETAG", [key, 0]); + _ = db.Execute("SET", [key, 0, "WITHETAG"]); _ = db.Execute("INCRBY", [key, ulong.MaxValue.ToString()]); break; case RespCommand.DECRBY: - _ = db.Execute("SETWITHETAG", [key, 0]); + _ = db.Execute("SET", [key, 0, "WITHETAG"]); _ = db.Execute("DECRBY", [key, ulong.MaxValue.ToString()]); break; } @@ -882,7 +825,7 @@ public void SimpleIncrementByFloatForEtagSetData(double initialValue, double inc using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); var key = "key1"; - db.Execute("SETWITHETAG", key, initialValue); + db.Execute("SET", key, initialValue, "WITHETAG"); var expectedResult = initialValue + incrByValue; @@ -911,7 +854,7 @@ public void SingleDeleteForEtagSetData() // Key storing integer var nVal = 100; var strKey = "key1"; - db.Execute("SETWITHETAG", [strKey, nVal]); + db.Execute("SET", [strKey, nVal, "WITHETAG"]); db.KeyDelete(strKey); var retVal = Convert.ToBoolean(db.StringGet(strKey)); ClassicAssert.AreEqual(retVal, false); @@ -931,7 +874,7 @@ public void SingleDeleteWithObjectStoreDisabledForEtagSetData() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", [key, value, "WITHETAG"]); var resp = (string)db.StringGet(key); ClassicAssert.AreEqual(resp, value); @@ -970,7 +913,7 @@ public void SingleDeleteWithObjectStoreDisable_LTMForEtagSetData() { data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); var pair = data.Last(); - db.Execute("SETWITHETAG", [pair.Item1, pair.Item2]); + db.Execute("SET", [pair.Item1, pair.Item2, "WITHETAG"]); } @@ -1013,7 +956,7 @@ public void MultiKeyDeleteForEtagSetData([Values] bool withoutObjectStore) { data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); var pair = data.Last(); - db.Execute("SETWITHETAG", [pair.Item1, pair.Item2]); + db.Execute("SET", [pair.Item1, pair.Item2, "WITHETAG"]); } var keys = data.Select(x => (RedisKey)x.Item1).ToArray(); @@ -1048,7 +991,7 @@ public void MultiKeyUnlinkForEtagSetData([Values] bool withoutObjectStore) { data.Add(new Tuple(GetRandomString(keyLen), GetRandomString(valLen))); var pair = data.Last(); - db.Execute("SETWITHETAG", [pair.Item1, pair.Item2]); + db.Execute("SET", [pair.Item1, pair.Item2, "WITHETAG"]); } var keys = data.Select(x => (object)x.Item1).ToArray(); @@ -1077,7 +1020,7 @@ public void SingleExistsForEtagSetData([Values] bool withoutObjectStore) var strKey = "key1"; ClassicAssert.IsFalse(db.KeyExists(strKey)); - db.Execute("SETWITHETAG", [strKey, nVal]); + db.Execute("SET", [strKey, nVal, "WITHETAG"]); bool fExists = db.KeyExists("key1", CommandFlags.None); ClassicAssert.AreEqual(fExists, true); @@ -1101,191 +1044,102 @@ public void MultipleExistsKeysAndObjectsAndEtagData() db.StringSet("foo", "bar"); - db.Execute("SETWITHETAG", ["rizz", "bar"]); + db.Execute("SET", ["rizz", "bar", "WITHETAG"]); var exists = db.KeyExists(["key", "listKey", "zset:test", "foo", "rizz"]); ClassicAssert.AreEqual(4, exists); } - [Test] - public void SingleRenameEtagSetData() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string origValue = "test1"; - long etag = long.Parse(db.Execute("SETWITHETAG", ["key1", origValue]).ToString()); - ClassicAssert.AreEqual(0, etag); - - db.KeyRename("key1", "key2"); - string retValue = db.StringGet("key2"); - - ClassicAssert.AreEqual(origValue, retValue); - - // other key now gives no result - ClassicAssert.AreEqual("", db.Execute("GETWITHETAG", ["key1"]).ToString()); - - // new Key value pair created with older value, the etag is reset here back to 0 - var res = (RedisResult[])db.Execute("GETWITHETAG", ["key2"]); - ClassicAssert.AreEqual("0", res[0].ToString()); - ClassicAssert.AreEqual(origValue, res[1].ToString()); + #region RENAME - origValue = db.StringGet("key1"); - ClassicAssert.AreEqual(null, origValue); - } [Test] - public void SingleRenameEtagShouldRetainEtagOfNewKeyIfExistsWithEtag() + public void RenameEtagTests() { + // old key had etag => new key zero'd etag when made without withetag (new key did not exists) + // old key had etag => new key zero'd etag when made without withetag (new key exists without etag) + // old key had etag => new key has updated etag when made with withetag (new key exists withetag) + // old key not have etag => new key made with updated etag when made withetag (new key did exist withetag) + // old key had etag and, new key has initial etag when made with withetag (new key did not exists) + // old key not have etag and, new key made with initial etag when made withetag (new key did not exist) using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string existingNewKey = "key2"; - string existingVal = "foo"; - long etag = (long)db.Execute("SETWITHETAG", [existingNewKey, existingVal]); - ClassicAssert.AreEqual(0, etag); - RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", [existingNewKey, "updated", etag.ToString()]); - long updatedEtag = (long)updateRes[0]; + IDatabase db = redis.GetDatabase(0); string origValue = "test1"; - etag = long.Parse(db.Execute("SETWITHETAG", ["key1", origValue]).ToString()); - ClassicAssert.AreEqual(0, etag); - - db.KeyRename("key1", existingNewKey); - string retValue = db.StringGet(existingNewKey); - ClassicAssert.AreEqual(origValue, retValue); + string oldKey = "key1"; + string newKey = "key2"; - // new Key value pair created with older value, the etag is reusing the existingnewkey etag - var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); - ClassicAssert.AreEqual(updatedEtag + 1, (long)res[0]); - ClassicAssert.AreEqual(origValue, res[1].ToString()); + // Scenario: old key had etag and => new key zero'd etag when made without withetag (new key did not exists) - origValue = db.StringGet("key1"); - ClassicAssert.AreEqual(null, origValue); - } + long etag = long.Parse(db.Execute("SET", [oldKey, origValue, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, etag); - [Test] - public void SingleRenameKeyEdgeCaseEtagSetData([Values] bool withoutObjectStore) - { - if (withoutObjectStore) - { - TearDown(); - TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true); - server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir, DisableObjects: true); - server.Start(); - } - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); + db.KeyRename(oldKey, newKey); - //1. Key rename does not exist - try - { - var res = db.KeyRename("key1", "key2"); - } - catch (Exception ex) - { - ClassicAssert.AreEqual("ERR no such key", ex.Message); - } + ClassicAssert.IsTrue(db.StringGet(oldKey).IsNull); + ClassicAssert.IsTrue(EtagAndValMatches(db, newKey, 0, origValue)); + // old key has been deleted, and new key exists without etag at this point - //2. Key rename oldKey.Equals(newKey) - string origValue = "test1"; - db.Execute("SETWITHETAG", ["key1", origValue]); + // Scenario: old key had etag => new key zero'd etag when made without withetag (new key exists without etag) + db.Execute("SET", oldKey, origValue, "WITHETAG"); - bool renameRes = db.KeyRename("key1", "key1"); - ClassicAssert.IsTrue(renameRes); - string retValue = db.StringGet("key1"); - ClassicAssert.AreEqual(origValue, retValue); - } + db.KeyRename(oldKey, newKey); - [Test] - public void SingleRenameShouldNotAddEtagEvenIfExistingKeyHadEtagButNotTheOriginal() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); + ClassicAssert.IsTrue(db.StringGet(oldKey).IsNull); + ClassicAssert.IsTrue(EtagAndValMatches(db, newKey, 0, origValue)); + db.KeyDelete(newKey); - string existingNewKey = "key2"; - string existingVal = "foo"; - long etag = (long)db.Execute("SETWITHETAG", [existingNewKey, existingVal]); - ClassicAssert.AreEqual(0, etag); - RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", [existingNewKey, "updated", etag.ToString()]); - long updatedEtag = (long)updateRes[0]; + // Scenario: old key had etag => new key has updated etag when made with withetag (new key exists withetag) + // setup new key with updated etag + db.Execute("SET", newKey, origValue + "delta", "WITHETAG"); + db.Execute("SETIFMATCH", newKey, origValue, 1); // updates etag to 2 + // old key with etag + etag = long.Parse(db.Execute("SET", [oldKey, origValue, "WITHETAG"]).ToString()); + ClassicAssert.AreEqual(1, etag); - string origValue = "test1"; - ClassicAssert.IsTrue(db.StringSet("key1", origValue)); + db.Execute("RENAME", oldKey, newKey, "WITHETAG"); // should update etag to 3 - db.KeyRename("key1", existingNewKey); - string retValue = db.StringGet(existingNewKey); - ClassicAssert.AreEqual(origValue, retValue); + ClassicAssert.IsTrue(db.StringGet(oldKey).IsNull); + ClassicAssert.IsTrue(EtagAndValMatches(db, newKey, 3, origValue)); + // at this point new key exists with etag, old key does not exist at all - // new Key value pair created with older value, the etag is reusing the existingnewkey etag - var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); - ClassicAssert.IsTrue(res[0].IsNull); - ClassicAssert.AreEqual(origValue, res[1].ToString()); + // Scenario: old key not have etag => new key made with updated etag when made withetag (new key did exist withetag) + db.Execute("SET", oldKey, origValue); - origValue = db.StringGet("key1"); - ClassicAssert.AreEqual(null, origValue); - } + db.Execute("RENAME", oldKey, newKey, "WITHETAG"); - [Test] - public void SingleRenameShouldAddEtagIfOldKeyHadEtagButNotExistingNewkey() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); + ClassicAssert.IsTrue(db.StringGet(oldKey).IsNull); + ClassicAssert.IsTrue(EtagAndValMatches(db, newKey, 4, origValue)); + db.KeyDelete(newKey); - string existingNewKey = "key2"; - string existingVal = "foo"; + // Scenario: old key had etag => new key has initial etag when made with withetag (new key did not exists) + db.Execute("SET", oldKey, origValue, "WITHETAG"); - ClassicAssert.IsTrue(db.StringSet(existingNewKey, existingVal)); + db.Execute("RENAME", oldKey, newKey, "WITHETAG"); - string origValue = "test1"; - long etag = (long)db.Execute("SETWITHETAG", ["key1", origValue]); - ClassicAssert.AreEqual(0, etag); + ClassicAssert.IsTrue(db.StringGet(oldKey).IsNull); + ClassicAssert.IsTrue(EtagAndValMatches(db, newKey, 1, origValue)); + db.KeyDelete(newKey); - db.KeyRename("key1", existingNewKey); - string retValue = db.StringGet(existingNewKey); - ClassicAssert.AreEqual(origValue, retValue); + // Scenario: old key not have etag => new key made with initial etag when made withetag (new key did not exist) + db.Execute("SET", oldKey, origValue); - // new Key value pair created with older value - var res = (RedisResult[])db.Execute("GETWITHETAG", [existingNewKey]); - ClassicAssert.AreEqual(0, (long)res[0]); - ClassicAssert.AreEqual(origValue, res[1].ToString()); + db.Execute("RENAME", oldKey, newKey, "WITHETAG"); - origValue = db.StringGet("key1"); - ClassicAssert.AreEqual(null, origValue); + ClassicAssert.IsTrue(db.StringGet(oldKey).IsNull); + ClassicAssert.IsTrue(EtagAndValMatches(db, newKey, 1, origValue)); + db.KeyDelete(newKey); } - [Test] - public void SingleRenameShouldAddEtagAndMetadataIfOldKeyHadEtagAndMetadata() + private bool EtagAndValMatches(IDatabase db, string key, long expectedEtag, string expectedValue) { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - - string origKey = "key1"; - string origValue = "test1"; - long etag = (long)db.Execute("SETWITHETAG", ["key1", origValue]); - ClassicAssert.AreEqual(0, etag); - - ClassicAssert.IsTrue(db.KeyExpire(origKey, TimeSpan.FromSeconds(10))); - - string newKey = "key2"; - db.KeyRename(origKey, newKey); - - string retValue = db.StringGet(newKey); - ClassicAssert.AreEqual(origValue, retValue); - - // new Key value pair created with older value - var res = (RedisResult[])db.Execute("GETWITHETAG", [newKey]); - ClassicAssert.AreEqual(0, (long)res[0]); - ClassicAssert.AreEqual(origValue, res[1].ToString()); - - // check that the ttl is not empty on new key because it inherited it from prev key - TimeSpan? ttl = db.KeyTimeToLive(newKey); - ClassicAssert.IsNotNull(ttl); - - origValue = db.StringGet(origKey); - ClassicAssert.AreEqual(null, origValue); + var res = (RedisResult[])db.Execute("GETWITHETAG", key); + var responseEtag = long.Parse(res[0].ToString()); + var responseValue = res[1].ToString(); + return responseValue == expectedValue && responseEtag == expectedEtag; } + #endregion [Test] public void PersistTTLTestForEtagSetData() @@ -1300,14 +1154,14 @@ public void PersistTTLTestForEtagSetData() var ttl = db.Execute("TTL", key); ClassicAssert.AreEqual(-2, (int)ttl); - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SET", [key, val, "WITHETAG"]); ttl = db.Execute("TTL", key); ClassicAssert.AreEqual(-1, (int)ttl); db.KeyExpire(key, TimeSpan.FromSeconds(expire)); var res = (RedisResult[])db.Execute("GETWITHETAG", [key]); - ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(1, long.Parse(res[0].ToString())); ClassicAssert.AreEqual(val, res[1].ToString()); var time = db.KeyTimeToLive(key); @@ -1344,7 +1198,7 @@ public void PersistTestForEtagSetData() int expire = 100; var keyA = "keyA"; - db.Execute("SETWITHETAG", [keyA, keyA]); + db.Execute("SET", [keyA, keyA, "WITHETAG"]); var response = db.KeyPersist(keyA); ClassicAssert.IsFalse(response); @@ -1363,7 +1217,7 @@ public void PersistTestForEtagSetData() ClassicAssert.AreEqual(value, keyA); var res = (RedisResult[])db.Execute("GETWITHETAG", [keyA]); - ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(1, long.Parse(res[0].ToString())); ClassicAssert.AreEqual(keyA, res[1].ToString()); var noKey = "noKey"; @@ -1380,7 +1234,7 @@ public void KeyExpireStringTestForEtagSetData(string command) var db = redis.GetDatabase(0); var key = "keyA"; - db.Execute("SETWITHETAG", [key, key]); + db.Execute("SET", [key, key]); var value = db.StringGet(key); ClassicAssert.AreEqual(key, (string)value); @@ -1406,7 +1260,7 @@ public void KeyExpireOptionsTestForEtagSetData(string command) var key = "keyA"; object[] args = [key, 1000, ""]; - db.Execute("SETWITHETAG", [key, key]); + db.Execute("SET", [key, key, "WITHETAG"]); args[2] = "XX";// XX -- Set expiry only when the key has an existing expiry bool resp = (bool)db.Execute($"{command}", args); @@ -1469,8 +1323,7 @@ public void MainObjectKeyForEtagSetData() const string key = "test:1"; - // Do SETIWTHETAG - ClassicAssert.AreEqual(0, long.Parse(db.Execute("SETWITHETAG", [key, "v1"]).ToString())); + ClassicAssert.AreEqual(1, long.Parse(db.Execute("SET", key, "v1", "WITHETAG").ToString())); // Do SetAdd using the same key ClassicAssert.IsTrue(db.SetAdd(key, "v2")); @@ -1501,7 +1354,7 @@ public void GetSliceTestForEtagSetData() var resp = (string)db.StringGetRange(key, 2, 10); ClassicAssert.AreEqual(string.Empty, resp); - ClassicAssert.AreEqual(0, long.Parse(db.Execute("SETWITHETAG", [key, value]).ToString())); + ClassicAssert.AreEqual(1, long.Parse(db.Execute("SET", key, value, "WITHETAG").ToString())); //0,0 resp = (string)db.StringGetRange(key, 0, 0); @@ -1617,7 +1470,7 @@ public void SetRangeTestForEtagSetData() string value = "0123456789"; string newValue = "ABCDE"; - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", key, value, "WITHETAG"); var resp = db.StringGet(key); ClassicAssert.AreEqual("0123456789", resp.ToString()); @@ -1630,7 +1483,7 @@ public void SetRangeTestForEtagSetData() // should update the etag internally var updatedEtagRes = db.Execute("GETWITHETAG", key); - ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); + ClassicAssert.AreEqual(2, long.Parse(updatedEtagRes[0].ToString())); ClassicAssert.IsTrue(db.KeyDelete(key)); @@ -1646,7 +1499,7 @@ public void SetRangeTestForEtagSetData() } // existing key, length 10, offset 0, value length 5 -> 10 ("ABCDE56789") - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", key, value, "WITHETAG"); resp = db.StringSetRange(key, 0, newValue); ClassicAssert.AreEqual("10", resp.ToString()); @@ -1660,7 +1513,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.IsTrue(db.KeyDelete(key)); // key, length 10, offset 5, value length 5 -> 10 ("01234ABCDE") - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", key, value, "WITHETAG"); resp = db.StringSetRange(key, 5, newValue); ClassicAssert.AreEqual("10", resp.ToString()); @@ -1673,7 +1526,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.IsTrue(db.KeyDelete(key)); // existing key, length 10, offset 10, value length 5 -> 15 ("0123456789ABCDE") - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", [key, value, "WITHETAG"]); resp = db.StringSetRange(key, 10, newValue); ClassicAssert.AreEqual("15", resp.ToString()); resp = db.StringGet(key); @@ -1681,7 +1534,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.IsTrue(db.KeyDelete(key)); // existing key, length 10, offset 15, value length 5 -> 20 ("0123456789\0\0\0\0\0ABCDE") - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", [key, value, "WITHETAG"]); resp = db.StringSetRange(key, 15, newValue); ClassicAssert.AreEqual("20", resp.ToString()); @@ -1690,7 +1543,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.IsTrue(db.KeyDelete(key)); // existing key, length 10, offset -1, value length 5 -> RedisServerException ("ERR offset is out of range") - db.Execute("SETWITHETAG", [key, value]); + db.Execute("SET", [key, value, "WITHETAG"]); try { db.StringSetRange(key, -1, newValue); @@ -1703,7 +1556,7 @@ public void SetRangeTestForEtagSetData() } [Test] - public void KeepTtlTestForDataInitiallySetWithEtag() + public void KeepTtlTestForDataInitiallySET() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -1711,8 +1564,8 @@ public void KeepTtlTestForDataInitiallySetWithEtag() int expire = 3; var keyA = "keyA"; var keyB = "keyB"; - db.Execute("SETWITHETAG", [keyA, keyA]); - db.Execute("SETWITHETAG", [keyB, keyB]); + db.Execute("SET", [keyA, keyA]); + db.Execute("SET", [keyB, keyB]); db.KeyExpire(keyA, TimeSpan.FromSeconds(expire)); db.KeyExpire(keyB, TimeSpan.FromSeconds(expire)); @@ -1740,13 +1593,13 @@ public void StrlenTestOnEtagSetData() using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); - db.Execute("SETWITHETAG", ["mykey", "foo bar"]); + db.Execute("SET", ["mykey", "foo bar", "WITHETAG"]); ClassicAssert.AreEqual(7, db.StringLength("mykey")); ClassicAssert.AreEqual(0, db.StringLength("nokey")); var etagToCheck = db.Execute("GETWITHETAG", "mykey"); - ClassicAssert.AreEqual(0, long.Parse(etagToCheck[0].ToString())); + ClassicAssert.AreEqual(1, long.Parse(etagToCheck[0].ToString())); } [Test] @@ -1762,7 +1615,7 @@ public void TTLTestMillisecondsForEtagSetData() var pttl = db.Execute("PTTL", key); ClassicAssert.AreEqual(-2, (int)pttl); - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SET", [key, val, "WITHETAG"]); pttl = db.Execute("PTTL", key); ClassicAssert.AreEqual(-1, (int)pttl); @@ -1786,7 +1639,7 @@ public void TTLTestMillisecondsForEtagSetData() // nothing should have affected the etag in the above commands long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(0, etagToCheck); + ClassicAssert.AreEqual(1, etagToCheck); } [Test] @@ -1799,7 +1652,7 @@ public void GetDelTestForEtagSetData() var val = "myKeyValue"; // Key Setup - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SET", [key, val, "WITHETAG"]); var retval = db.StringGet(key); ClassicAssert.AreEqual(val, retval.ToString()); @@ -1819,7 +1672,7 @@ public void GetDelTestForEtagSetData() key = "myKeyWithMetadata"; val = "myValueWithMetadata"; - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SET", [key, val, "WITHETAG"]); db.KeyExpire(key, TimeSpan.FromSeconds(10000)); retval = db.StringGet(key); @@ -1843,7 +1696,7 @@ public void AppendTestForEtagSetData() var val = "myKeyValue"; var val2 = "myKeyValue2"; - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SET", [key, val, "WITHETAG"]); var len = db.StringAppend(key, val2); ClassicAssert.AreEqual(val.Length + val2.Length, len); @@ -1852,12 +1705,12 @@ public void AppendTestForEtagSetData() ClassicAssert.AreEqual(val + val2, _val.ToString()); long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(1, etagToCheck); + ClassicAssert.AreEqual(2, etagToCheck); db.KeyDelete(key); // Test appending an empty string - db.Execute("SETWITHETAG", [key, val]); + db.Execute("SET", [key, val, "WITHETAG"]); var len1 = db.StringAppend(key, ""); ClassicAssert.AreEqual(val.Length, len1); @@ -1881,7 +1734,7 @@ public void AppendTestForEtagSetData() // Test appending to a key with a large value var largeVal = new string('a', 1000000); - db.Execute("SETWITHETAG", [key, largeVal]); + db.Execute("SET", [key, largeVal, "WITHETAG"]); var len3 = db.StringAppend(key, val2); ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); @@ -1890,7 +1743,7 @@ public void AppendTestForEtagSetData() // Test appending to a key with metadata var keyWithMetadata = "keyWithMetadata"; - db.Execute("SETWITHETAG", [keyWithMetadata, val]); + db.Execute("SET", [keyWithMetadata, val, "WITHETAG"]); db.KeyExpire(keyWithMetadata, TimeSpan.FromSeconds(10000)); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [keyWithMetadata]))[0].ToString()); ClassicAssert.AreEqual(0, etagToCheck); @@ -1919,13 +1772,13 @@ public void SetBitOperationsOnEtagSetData() Byte[] initialBitmap = new byte[8]; string bitMapAsStr = Encoding.UTF8.GetString(initialBitmap); ; - db.Execute("SETWITHETAG", [key, bitMapAsStr]); + db.Execute("SET", [key, bitMapAsStr, "WITHETAG"]); long setbits = db.StringBitCount(key); ClassicAssert.AreEqual(0, setbits); long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(0, etagToCheck); + ClassicAssert.AreEqual(1, etagToCheck); // set all 64 bits one by one var expectedBitCount = 0; @@ -1997,13 +1850,13 @@ public void BitFieldSetGetOnEtagSetData() var key = "mewo"; // Arrange - Set an 8-bit unsigned value at offset 0 - db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte + db.Execute("SET", [key, Encoding.UTF8.GetString(new byte[1]), "WITHETAG"]); // Initialize key with an empty byte // Act - Set value to 127 (binary: 01111111) db.Execute("BITFIELD", key, "SET", "u8", "0", "127"); long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(1, etagToCheck); + ClassicAssert.AreEqual(2, etagToCheck); // Get value back var getResult = (RedisResult[])db.Execute("BITFIELD", key, "GET", "u8", "0"); @@ -2021,12 +1874,12 @@ public void BitFieldIncrementWithWrapOverflowOnEtagSetData() var key = "mewo"; // Arrange - Set an 8-bit unsigned value at offset 0 - db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte + db.Execute("SET", [key, Encoding.UTF8.GetString(new byte[1]), "WITHETAG"]); // Initialize key with an empty byte // Act - Set initial value to 255 and try to increment by 1 db.Execute("BITFIELD", key, "SET", "u8", "0", "255"); long etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(1, etagToCheck); + ClassicAssert.AreEqual(2, etagToCheck); var incrResult = db.Execute("BITFIELD", key, "INCRBY", "u8", "0", "1"); @@ -2046,7 +1899,7 @@ public void BitFieldIncrementWithSaturateOverflowOnEtagSetData() var key = "mewo"; // Arrange - Set an 8-bit unsigned value at offset 0 - db.Execute("SETWITHETAG", [key, Encoding.UTF8.GetString(new byte[1])]); // Initialize key with an empty byte + db.Execute("SET", [key, Encoding.UTF8.GetString(new byte[1]), "WITHETAG"]); // Initialize key with an empty byte // Act - Set initial value to 250 and try to increment by 10 with saturate overflow var bitfieldRes = db.Execute("BITFIELD", key, "SET", "u8", "0", "250"); @@ -2054,7 +1907,7 @@ public void BitFieldIncrementWithSaturateOverflowOnEtagSetData() var result = (RedisResult[])db.Execute("GETWITHETAG", [key]); long etagToCheck = long.Parse(result[0].ToString()); - ClassicAssert.AreEqual(1, etagToCheck); + ClassicAssert.AreEqual(2, etagToCheck); var incrResult = db.Execute("BITFIELD", key, "OVERFLOW", "SAT", "INCRBY", "u8", "0", "10"); @@ -2074,8 +1927,8 @@ public void HyperLogLogCommandsShouldReturnWrongTypeErrorForEtagSetData() var key = "mewo"; var key2 = "dude"; - db.Execute("SETWITHETAG", [key, "mars"]); - db.Execute("SETWITHETAG", [key2, "marsrover"]); + db.Execute("SET", [key, "mars", "WITHETAG"]); + db.Execute("SET", [key2, "marsrover", "WITHETAG"]); RedisServerException ex = Assert.Throws(() => db.Execute("PFADD", [key, "woohoo"])); @@ -2089,7 +1942,7 @@ public void HyperLogLogCommandsShouldReturnWrongTypeErrorForEtagSetData() } [Test] - public void SetWithRetainEtagOnANewUpsertWillCreateKeyValueWithoutEtag() + public void SetWithWITHETAGOnANewUpsertWillCreateKeyValueWithoutEtag() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); @@ -2098,24 +1951,24 @@ public void SetWithRetainEtagOnANewUpsertWillCreateKeyValueWithoutEtag() string val = "mouse"; // a new upsert on a non-existing key will retain the "nil" etag - db.Execute("SET", [key, val, "RETAINETAG"]).ToString(); + db.Execute("SET", [key, val, "WITHETAG"]).ToString(); RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", [key]); RedisResult etag = res[0]; string value = res[1].ToString(); - ClassicAssert.IsTrue(etag.IsNull); + ClassicAssert.AreEqual("1", etag.ToString()); ClassicAssert.AreEqual(val, value); string newval = "clubhouse"; - // a new upsert on an existing key will retain the "nil" etag from the prev - db.Execute("SET", [key, newval, "RETAINETAG"]).ToString(); + // a new upsert on an existing key will reset the etag on the key + db.Execute("SET", [key, newval]).ToString(); res = (RedisResult[])db.Execute("GETWITHETAG", [key]); etag = res[0]; value = res[1].ToString(); - ClassicAssert.IsTrue(etag.IsNull); + ClassicAssert.AreEqual("0", etag.ToString()); ClassicAssert.AreEqual(newval, value); } diff --git a/test/Garnet.test/RespTests.cs b/test/Garnet.test/RespTests.cs index 3fe01d25d0..585ff8eed2 100644 --- a/test/Garnet.test/RespTests.cs +++ b/test/Garnet.test/RespTests.cs @@ -1857,8 +1857,8 @@ public void SingleRenameNXWithEtagSetOldAndNewKey() var key = "key1"; var newKey = "key2"; - db.Execute("SETWITHETAG", key, origValue); - db.Execute("SETWITHETAG", newKey, "foo"); + db.Execute("SET", key, origValue, "WITHETAG"); + db.Execute("SET", newKey, "foo", "WITHETAG"); var result = db.KeyRename(key, newKey, When.NotExists); ClassicAssert.IsFalse(result); @@ -1873,7 +1873,7 @@ public void SingleRenameNXWithEtagSetOldKey() var key = "key1"; var newKey = "key2"; - db.Execute("SETWITHETAG", key, origValue); + db.Execute("SET", key, origValue, "WITHETAG"); var result = db.KeyRename(key, newKey, When.NotExists); ClassicAssert.IsTrue(result); diff --git a/test/Garnet.test/TransactionTests.cs b/test/Garnet.test/TransactionTests.cs index 0fde102b8f..c93bf2ae18 100644 --- a/test/Garnet.test/TransactionTests.cs +++ b/test/Garnet.test/TransactionTests.cs @@ -226,7 +226,7 @@ public async Task WatchTestWithSetWithEtag() byte[] res; string expectedResponse = ":0\r\n"; - res = lightClientRequest.SendCommand("SETWITHETAG key1 value1"); + res = lightClientRequest.SendCommand("SET key1 value1 WITHETAG"); ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); expectedResponse = "+OK\r\n"; @@ -242,7 +242,7 @@ public async Task WatchTestWithSetWithEtag() res = lightClientRequest.SendCommand("SET key2 value2"); ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); - await Task.Run(() => updateKey("key1", "value1_updated", retainEtag: true)); + await Task.Run(() => updateKey("key1", "value1_updated", withEtag: true)); res = lightClientRequest.SendCommand("EXEC"); expectedResponse = "*-1"; @@ -254,17 +254,17 @@ public async Task WatchTestWithSetWithEtag() lightClientRequest.SendCommand("GET key1"); lightClientRequest.SendCommand("SET key2 value2"); // check that all the etag commands can be called inside a transaction - lightClientRequest.SendCommand("SETWITHETAG key3 value2"); + lightClientRequest.SendCommand("SET key3 value2 WITHETAG "); lightClientRequest.SendCommand("GETWITHETAG key3"); lightClientRequest.SendCommand("GETIFNOTMATCH key3 0"); lightClientRequest.SendCommand("SETIFMATCH key3 anotherVal 0"); - lightClientRequest.SendCommand("SETWITHETAG key3 arandomval RETAINETAG"); + lightClientRequest.SendCommand("SET key3 arandomval WITHETAG"); res = lightClientRequest.SendCommand("EXEC"); - expectedResponse = "*7\r\n$14\r\nvalue1_updated\r\n+OK\r\n:0\r\n*2\r\n:0\r\n$6\r\nvalue2\r\n+NOTCHANGED\r\n*2\r\n:1\r\n$10\r\nanotherVal\r\n:2\r\n"; + expectedResponse = "*7\r\n$14\r\nvalue1_updated\r\n+OK\r\n:0\r\n*2\r\n:0\r\n$6\r\nvalue2\r\n*2\r\n:0\r\n$-1\r\n*2\r\n:1\r\n$10\r\nanotherVal\r\n:2\r\n"; string response = Encoding.ASCII.GetString(res.AsSpan().Slice(0, expectedResponse.Length)); - ClassicAssert.AreEqual(response, expectedResponse); + ClassicAssert.AreEqual(expectedResponse, response); // check if we still have the appropriate etag on the key we had set var otherLighClientRequest = TestUtils.CreateRequest(); @@ -361,11 +361,11 @@ public async Task WatchKeyFromDisk() ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); } - private static void updateKey(string key, string value, bool retainEtag = false) + private static void updateKey(string key, string value, bool withEtag = false) { using var lightClientRequest = TestUtils.CreateRequest(); string command = $"SET {key} {value}"; - command += retainEtag ? " RETAINETAG" : ""; + command += withEtag ? " WITHETAG" : ""; byte[] res = lightClientRequest.SendCommand(command); string expectedResponse = "+OK\r\n"; ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index a3c48615e9..07610d8b44 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -131,26 +131,6 @@ Compatibility with non-ETag commands and the behavior of data inserted with ETag --- -### **SETWITHETAG** - -#### **Syntax** - -```bash -SETWITHETAG key value [RETAINETAG] -``` - -Inserts a key-value string pair into Garnet, associating an ETag that will be updated upon changes to the value. - -**Options:** - -* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If not etag existed for the previous key this will initialize one. - -#### **Response** - -- **Integer reply**: A response integer indicating the initial ETag value on success. - ---- - ### **GETWITHETAG** #### **Syntax** @@ -186,7 +166,7 @@ One of the following: - **Integer reply**: The updated ETag if the value was successfully updated. - **Nil reply**: If the key does not exist. -- **Simple string reply**: If the provided ETag does not match the current ETag or If the command is called on a record without an ETag a simple string indicating ETag mismatch is returned. +- **Simple string reply**: If the provided ETag does not match the current ETag returns VAL_NOT_FOUND --- @@ -204,9 +184,8 @@ Retrieves the value if the ETag associated with the key has changed; otherwise, One of the following: -- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be nil. +- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be 0. If the etag matches then we the first item on the array is the existing etag, but the second value is Nil - **Nil reply**: If the key does not exist. -- **Simple string reply**: if the provided ETag matches the current ETag, returns a simple string indicating the value is unchanged. --- @@ -216,7 +195,7 @@ Below is the expected behavior of ETag-associated key-value pairs when non-ETag - **MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. -- **SET**: Only if used with additional option "RETAINETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. +- **SET**: Only if used with additional option "WITHETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. - **RENAME**: Renaming an old ETag-associated key-value pair will create the newly renamed key with an initial etag of 0. If the key being renamed to already existed before hand, it will retain the etag of the existing key that was the target of the rename, and show it as an updated etag. diff --git a/website/docs/commands/raw-string.md b/website/docs/commands/raw-string.md index f869ae4169..00ab1e2f61 100644 --- a/website/docs/commands/raw-string.md +++ b/website/docs/commands/raw-string.md @@ -289,7 +289,7 @@ Simple string reply: OK. #### Syntax ```bash - SET key value [NX | XX] [GET] [EX seconds | PX milliseconds] [KEEPTTL] [RETAINETAG] + SET key value [NX | XX] [GET] [EX seconds | PX milliseconds] [KEEPTTL] [WITHETAG] ``` Set **key** to hold the string value. If key already holds a value, it is overwritten, regardless of its type. Any previous time to live associated with the **key** is discarded on successful SET operation. @@ -301,7 +301,7 @@ Set **key** to hold the string value. If key already holds a value, it is overwr * NX -- Only set the key if it does not already exist. * XX -- Only set the key if it already exists. * KEEPTTL -- Retain the time to live associated with the key. -* RETAINETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this will create the new key-value pair without any etag as well. This is a Garnet specific command, you can read more about ETag support [here](../commands/garnet-specific-commands#native-etag-support) +* WITHETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this will create the new key-value pair without any etag as well. This is a Garnet specific command, you can read more about ETag support [here](../commands/garnet-specific-commands#native-etag-support) #### Resp Reply From 7f5170d4c9591f47342c0b3303519e7026075bf5 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 19 Dec 2024 15:57:43 -0800 Subject: [PATCH 37/87] Beautiful patter matching done --- libs/server/Resp/BasicCommands.cs | 98 +++++++++++++++---------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index d38b044407..c9548e829f 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -701,10 +701,6 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) return true; } - // Make space for value header - var valPtr = sbVal.ToPointer() - sizeof(int); - var vSize = sbVal.Length; - bool withEtag = etagOption == EtagOption.WITHETAG; var isHighPrecision = expOption == ExpirationOption.PX; @@ -788,66 +784,66 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref if (withEtag) input.header.SetWithEtagFlag(); - + if (getValue) input.header.SetSetGetFlag(); - // getValue and withEtag both need to write to memory something from the record + SpanByteAndMemory outputBuffer = default; + GarnetStatus status; + + // SETIFMATCH will always hit this conditional and have o point to the right memory if (getValue || withEtag) { - var o = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - var status = storageApi.SET_Conditional(ref key, - ref input, ref o, cmd); - - // not found for a withEtag based set is okay, and so we invert it - if (withEtag && status == GarnetStatus.NOTFOUND) - { - status = GarnetStatus.OK; - } - - // Status tells us whether an old image was found during RMW or not - switch (status) - { - case GarnetStatus.NOTFOUND: - Debug.Assert(o.IsSpanByte); - while (!RespWriteUtils.WriteNull(ref dcurr, dend)) - SendAndReset(); - break; - // SETIFNOTMATCH is the only one who can return this and is always called with getvalue so we only handle this here - case GarnetStatus.ETAGMISMATCH: - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) - SendAndReset(); - break; - default: - if (!o.IsSpanByte) - SendAndReset(o.Memory, o.Length); - else - dcurr += o.Length; - break; - } + // anything with getValue or withEtag may choose to write to the buffer in success scenarios + outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + status = storageApi.SET_Conditional(ref key, + ref input, ref outputBuffer, cmd); } else { - var status = storageApi.SET_Conditional(ref key, ref input); + // the following debug is the catch any edge case leading to SETIFMATCH skipping the above block + Debug.Assert(cmd != RespCommand.SETIFMATCH, "SETIFMATCH should have gone though pointing to right output variable"); - var ok = status != GarnetStatus.NOTFOUND; + status = storageApi.SET_Conditional(ref key, ref input); + } - // Status tells us whether an old image was found during RMW or not - // For a "set if not exists" NOTFOUND means the operation succeeded - // So we invert the ok flag - if (cmd == RespCommand.SETEXNX) - ok = !ok; - if (!ok) - { - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + switch ((getValue, withEtag, cmd, status)) + { + case (_, _, RespCommand.SETIFMATCH, GarnetStatus.ETAGMISMATCH): // write back mismatch error + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) SendAndReset(); - } - else - { + break; + + case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND): // since SET with etag goes down RMW a not found is okay and data is on buffer + // if getvalue || etag and Status is OK then the response is always on the buffer, getvalue is never used with conditionals + // extra pattern matching on command below for invariant get value cannot be used with EXXX and EXNX + case (true, _, RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETKEEPTTL, GarnetStatus.OK): + case (_, true, _, GarnetStatus.OK): + if (!outputBuffer.IsSpanByte) + SendAndReset(outputBuffer.Memory, outputBuffer.Length); + else + dcurr += outputBuffer.Length; + break; + + case (false, false, RespCommand.SETEXNX, GarnetStatus.NOTFOUND): // SETEXNX is at success if not found and nothign on buffer if no get or withetag so return +OK + case (false, false, + RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETEXXX or RespCommand.SETKEEPTTL or RespCommand.SETKEEPTTLXX, + GarnetStatus.OK): // for everything EXCPET SETEXNX if no get, and no etag, then an OK returns +OK response while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); - } - } + break; + + case (_, _, RespCommand.SETEXNX, GarnetStatus.OK): // For NX semantics an OK indicates a found, which means nothing was set and hence we return NIL + // anything not found that did not come from SETEXNX always returns NIL, also anything that is indicating wrong type or moved will return NIL + case (_, _, _, GarnetStatus.NOTFOUND or GarnetStatus.WRONGTYPE or GarnetStatus.MOVED): + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + + default: + Debug.Assert(false, $"({getValue}, {withEtag}, {cmd}, {status}) unaccounted for combination in response pattern matching. Please make explicit."); + break; + }; return true; } From 5c2337d9a85f1eddee3e88d0acec7ca950dc5daf Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 19 Dec 2024 17:25:44 -0800 Subject: [PATCH 38/87] SET working, todo remaining tests fix --- libs/server/Resp/BasicCommands.cs | 9 ++-- .../Storage/Functions/MainStore/RMWMethods.cs | 33 +++++++++++--- test/Garnet.test/RespEtagTests.cs | 43 ++++++++++--------- 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index c9548e829f..8b81ce30d6 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -814,11 +814,12 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref SendAndReset(); break; - case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND): // since SET with etag goes down RMW a not found is okay and data is on buffer + // since SET with etag goes down RMW a not found is okay and data is on buffer + case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND): // if getvalue || etag and Status is OK then the response is always on the buffer, getvalue is never used with conditionals // extra pattern matching on command below for invariant get value cannot be used with EXXX and EXNX case (true, _, RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETKEEPTTL, GarnetStatus.OK): - case (_, true, _, GarnetStatus.OK): + case (_, true, _, GarnetStatus.OK or GarnetStatus.NOTFOUND): if (!outputBuffer.IsSpanByte) SendAndReset(outputBuffer.Memory, outputBuffer.Length); else @@ -834,8 +835,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref break; case (_, _, RespCommand.SETEXNX, GarnetStatus.OK): // For NX semantics an OK indicates a found, which means nothing was set and hence we return NIL - // anything not found that did not come from SETEXNX always returns NIL, also anything that is indicating wrong type or moved will return NIL - case (_, _, _, GarnetStatus.NOTFOUND or GarnetStatus.WRONGTYPE or GarnetStatus.MOVED): + // anything not found that did not come from SETEXNX or WITHETAG always returns NIL, also anything that is indicating wrong type or moved will return NIL + case (_, false, not RespCommand.SETEXNX, GarnetStatus.NOTFOUND or GarnetStatus.WRONGTYPE or GarnetStatus.MOVED): while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); break; diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2fa1e3d70d..af2d1a116a 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -21,7 +21,6 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp { case RespCommand.SETIFMATCH: case RespCommand.SETKEEPTTLXX: - case RespCommand.SETEXXX: case RespCommand.PERSIST: case RespCommand.EXPIRE: case RespCommand.PEXPIRE: @@ -30,6 +29,14 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.GETDEL: case RespCommand.GETEX: return false; + case RespCommand.SETEXXX: + // when called withetag all output needs to be placed on the buffer + if (input.header.CheckWithEtagFlag()) + { + // EXX when unsuccesful will write back NIL + CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + } + return false; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) { @@ -94,12 +101,8 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB { // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag *(long*)value.ToPointer() = Constants.BaseEtag + 1; - if (cmd == RespCommand.SET) - { - // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX - CopyRespNumber(Constants.BaseEtag + 1, ref output); - } - + // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX + CopyRespNumber(Constants.BaseEtag + 1, ref output); } break; @@ -306,6 +309,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } + + // when called withetag all output needs to be placed on the buffer + if (input.header.CheckWithEtagFlag()) + { + // EXX when unsuccesful will write back NIL + CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + } + // Nothing is set because being in this block means NX was already violated return true; case RespCommand.SETIFMATCH: @@ -787,6 +798,14 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB // Copy value to output for the GET part of the command. CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } + + // when called withetag all output needs to be placed on the buffer + if (input.header.CheckWithEtagFlag()) + { + // EXX when unsuccesful will write back NIL + CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + } + // since this block is only hit when this an update, the NX is violated and so we can return early from it without setting the value return false; case RespCommand.SETEXXX: diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 31deb2485f..4eef74290d 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -163,8 +163,7 @@ public void SetWithEtagWorksWithMetadata() var key2 = "key2"; db.Execute("SET", key2, "value2", "WITHETAG"); var res2 = db.Execute("SET", key2, "value3", "WITHETAG", "NX", "EX", 10); - // it 1 if key set, 0 if not set - ClassicAssert.AreEqual(1, (int)res2); + ClassicAssert.IsTrue(res2.IsNull); db.KeyDelete(key2); // Cleanup // Scenario: set with etag with expiration NX with non-existent key @@ -263,24 +262,24 @@ public void SETWithWITHETAGOnAlreadyExistingSETDataOverridesItButUpdatesEtag() // update to value to update the etag RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); etag = (long)updateRes[0]; - ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual(2, etag); ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); // inplace update res = db.Execute("SET", ["rizz", "meow", "WITHETAG"]); etag = (long)res; - ClassicAssert.AreEqual(2, etag); + ClassicAssert.AreEqual(3, etag); // update to value to update the etag updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); etag = (long)updateRes[0]; - ClassicAssert.AreEqual(3, etag); + ClassicAssert.AreEqual(4, etag); ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); // Copy update res = db.Execute("SET", ["rizz", "oneofus", "WITHETAG"]); etag = (long)res; - ClassicAssert.AreEqual(4, etag); + ClassicAssert.AreEqual(5, etag); } [Test] @@ -522,7 +521,7 @@ public void SetExpiryIncrForEtagSetData() // This should increase the ETAG internally so we have a check for that here checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(2, checkEtag); + ClassicAssert.AreEqual(3, checkEtag); nRetVal = Convert.ToInt64(db.StringGet(strKey)); ClassicAssert.AreEqual(n, nRetVal); @@ -567,7 +566,7 @@ public void IncrDecrChangeDigitsWithExpiry() ClassicAssert.AreEqual(10, nRetVal); checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(1, checkEtag); + ClassicAssert.AreEqual(2, checkEtag); n = db.StringDecrement(strKey); nRetVal = Convert.ToInt64(db.StringGet(strKey)); @@ -575,7 +574,7 @@ public void IncrDecrChangeDigitsWithExpiry() ClassicAssert.AreEqual(9, nRetVal); checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(2, checkEtag); + ClassicAssert.AreEqual(3, checkEtag); Thread.Sleep(TimeSpan.FromSeconds(5)); @@ -630,7 +629,7 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() ClassicAssert.AreEqual("ciaociao", retVal); var res = (RedisResult[])db.Execute("GETWITHETAG", strKey); - ClassicAssert.AreEqual(1, (long)res[0]); + ClassicAssert.AreEqual(2, (long)res[0]); // on subsequent upserts we are still increasing the etag transparently db.Execute("SET", [strKey, "ciaociaociao", "WITHETAG"]); @@ -639,7 +638,7 @@ public void StringSetOnAnExistingEtagDataUpdatesEtagIfEtagRetain() ClassicAssert.AreEqual("ciaociaociao", retVal); res = (RedisResult[])db.Execute("GETWITHETAG", strKey); - ClassicAssert.AreEqual(2, (long)res[0]); + ClassicAssert.AreEqual(3, (long)res[0]); ClassicAssert.AreEqual("ciaociaociao", res[1].ToString()); } @@ -1169,12 +1168,13 @@ public void PersistTTLTestForEtagSetData() db.KeyExpire(key, TimeSpan.FromSeconds(expire)); res = (RedisResult[])db.Execute("GETWITHETAG", [key]); - ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + ClassicAssert.AreEqual(1, long.Parse(res[0].ToString())); ClassicAssert.AreEqual(val, res[1].ToString()); db.KeyPersist(key); res = (RedisResult[])db.Execute("GETWITHETAG", [key]); - ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + // unchanged etag + ClassicAssert.AreEqual(1, long.Parse(res[0].ToString())); ClassicAssert.AreEqual(val, res[1].ToString()); Thread.Sleep((expire + 1) * 1000); @@ -1186,7 +1186,8 @@ public void PersistTTLTestForEtagSetData() ClassicAssert.IsNull(time); res = (RedisResult[])db.Execute("GETWITHETAG", [key]); - ClassicAssert.AreEqual(0, long.Parse(res[0].ToString())); + // the tag was persisted along with data from persist despite previous TTL + ClassicAssert.AreEqual(1, long.Parse(res[0].ToString())); ClassicAssert.AreEqual(val, res[1].ToString()); } @@ -1508,7 +1509,7 @@ public void SetRangeTestForEtagSetData() // should update the etag internally updatedEtagRes = db.Execute("GETWITHETAG", key); - ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); + ClassicAssert.AreEqual(2, long.Parse(updatedEtagRes[0].ToString())); ClassicAssert.IsTrue(db.KeyDelete(key)); @@ -1719,8 +1720,8 @@ public void AppendTestForEtagSetData() ClassicAssert.AreEqual(val, _val.ToString()); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - // we appended nothing so this remains 0 - ClassicAssert.AreEqual(0, etagToCheck); + // we appended nothing so this remains 1 + ClassicAssert.AreEqual(1, etagToCheck); // Test appending to a non-existent key var nonExistentKey = "nonExistentKey"; @@ -1739,14 +1740,14 @@ public void AppendTestForEtagSetData() ClassicAssert.AreEqual(largeVal.Length + val2.Length, len3); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(1, etagToCheck); + ClassicAssert.AreEqual(2, etagToCheck); // Test appending to a key with metadata var keyWithMetadata = "keyWithMetadata"; db.Execute("SET", [keyWithMetadata, val, "WITHETAG"]); db.KeyExpire(keyWithMetadata, TimeSpan.FromSeconds(10000)); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [keyWithMetadata]))[0].ToString()); - ClassicAssert.AreEqual(0, etagToCheck); + ClassicAssert.AreEqual(1, etagToCheck); var len4 = db.StringAppend(keyWithMetadata, val2); ClassicAssert.AreEqual(val.Length + val2.Length, len4); @@ -1758,7 +1759,7 @@ public void AppendTestForEtagSetData() ClassicAssert.IsTrue(time.Value.TotalSeconds > 0); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [keyWithMetadata]))[0].ToString()); - ClassicAssert.AreEqual(1, etagToCheck); + ClassicAssert.AreEqual(2, etagToCheck); } [Test] @@ -1912,7 +1913,7 @@ public void BitFieldIncrementWithSaturateOverflowOnEtagSetData() var incrResult = db.Execute("BITFIELD", key, "OVERFLOW", "SAT", "INCRBY", "u8", "0", "10"); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(2, etagToCheck); + ClassicAssert.AreEqual(3, etagToCheck); // Assert ClassicAssert.AreEqual(255, (long)incrResult); // Should saturate at the max value of 255 for u8 From e750564eaab1385a08675ad7ad9a480d812d1304 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 19 Dec 2024 22:06:18 -0800 Subject: [PATCH 39/87] Fix all tests --- test/Garnet.test/RespEtagTests.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 4eef74290d..5a9a8ed3be 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -699,7 +699,7 @@ public void SingleDecrForEtagSetData(string strKey, int nVal) ClassicAssert.AreEqual(n, nRetVal); long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(1, checkEtag); + ClassicAssert.AreEqual(2, checkEtag); } [Test] @@ -722,7 +722,7 @@ public void SingleDecrByForEtagSetData(long nVal, long nDecr) ClassicAssert.AreEqual(n, nRetVal); long checkEtag = long.Parse(db.Execute("GETWITHETAG", [strKey])[0].ToString()); - ClassicAssert.AreEqual(1, checkEtag); + ClassicAssert.AreEqual(2, checkEtag); } [Test] @@ -841,7 +841,7 @@ public void SimpleIncrementByFloatForEtagSetData(double initialValue, double inc long etag = (long)res[0]; double value = double.Parse(res[1].ToString(), CultureInfo.InvariantCulture); Assert.That(value, Is.EqualTo(actualResultRaw).Within(1.0 / Math.Pow(10, 15))); - ClassicAssert.AreEqual(1, etag); + ClassicAssert.AreEqual(2, etag); } [Test] @@ -1520,7 +1520,7 @@ public void SetRangeTestForEtagSetData() ClassicAssert.AreEqual("10", resp.ToString()); updatedEtagRes = db.Execute("GETWITHETAG", key); - ClassicAssert.AreEqual(1, long.Parse(updatedEtagRes[0].ToString())); + ClassicAssert.AreEqual(2, long.Parse(updatedEtagRes[0].ToString())); resp = db.StringGet(key); ClassicAssert.AreEqual("01234ABCDE", resp.ToString()); @@ -1782,7 +1782,8 @@ public void SetBitOperationsOnEtagSetData() ClassicAssert.AreEqual(1, etagToCheck); // set all 64 bits one by one - var expectedBitCount = 0; + long expectedBitCount = 0; + long expectedEtag = 1; for (int i = 0; i < 64; i++) { // SET the ith bit in the bitmap @@ -1790,6 +1791,7 @@ public void SetBitOperationsOnEtagSetData() ClassicAssert.IsFalse(originalValAtBit); expectedBitCount++; + expectedEtag++; bool currentBitVal = db.StringGetBit(key, i); ClassicAssert.IsTrue(currentBitVal); @@ -1809,10 +1811,9 @@ public void SetBitOperationsOnEtagSetData() // with each bit set that we do, we are increasing the etag as well by 1 etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(expectedBitCount, etagToCheck); + ClassicAssert.AreEqual(expectedEtag, etagToCheck); } - var expectedEtag = expectedBitCount; // unset all 64 bits one by one in reverse order for (int i = 63; i > -1; i--) { @@ -1885,7 +1886,7 @@ public void BitFieldIncrementWithWrapOverflowOnEtagSetData() var incrResult = db.Execute("BITFIELD", key, "INCRBY", "u8", "0", "1"); etagToCheck = long.Parse(((RedisResult[])db.Execute("GETWITHETAG", [key]))[0].ToString()); - ClassicAssert.AreEqual(2, etagToCheck); + ClassicAssert.AreEqual(3, etagToCheck); // Assert ClassicAssert.AreEqual(0, (long)incrResult); // Should wrap around and return 0 From 003a66070c5fdc9fe5c630beae830826fb24abe2 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 20 Dec 2024 01:18:03 -0800 Subject: [PATCH 40/87] Got everythign working --- libs/server/API/GarnetApi.cs | 4 +- libs/server/API/GarnetStatus.cs | 4 - libs/server/API/IGarnetApi.cs | 2 +- libs/server/Resp/BasicCommands.cs | 100 +++++++++++-- .../Storage/Functions/MainStore/RMWMethods.cs | 78 +++++----- .../Functions/MainStore/VarLenInputMethods.cs | 6 +- .../Session/MainStore/HyperLogLogOps.cs | 2 +- .../Storage/Session/MainStore/MainStoreOps.cs | 10 +- test/Garnet.test/RespEtagTests.cs | 136 ++++++++++++++++-- 9 files changed, 272 insertions(+), 70 deletions(-) diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index 9d1c0cf817..6c17967e17 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -127,8 +127,8 @@ public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input) => storageSession.SET_Conditional(ref key, ref input, ref context); /// - public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, RespCommand cmd) - => storageSession.SET_Conditional(ref key, ref input, ref output, ref context, cmd); + public GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output) + => storageSession.SET_Conditional(ref key, ref input, ref output, ref context); /// public GarnetStatus SET(ArgSlice key, Memory value) diff --git a/libs/server/API/GarnetStatus.cs b/libs/server/API/GarnetStatus.cs index 3e9c26eb02..0972da6fb6 100644 --- a/libs/server/API/GarnetStatus.cs +++ b/libs/server/API/GarnetStatus.cs @@ -24,9 +24,5 @@ public enum GarnetStatus : byte /// Wrong type /// WRONGTYPE, - /// - /// ETAG mismatch result for an etag based command - /// - ETAGMISMATCH, } } \ No newline at end of file diff --git a/libs/server/API/IGarnetApi.cs b/libs/server/API/IGarnetApi.cs index 82c9de2106..207111178c 100644 --- a/libs/server/API/IGarnetApi.cs +++ b/libs/server/API/IGarnetApi.cs @@ -41,7 +41,7 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi /// /// SET Conditional /// - GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, RespCommand cmd); + GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output); /// /// SET diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 8b81ce30d6..3422e88ebb 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -386,11 +386,96 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - Debug.Assert(parseState.Count == 3); + if (parseState.Count < 3 || parseState.Count > 5) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.SETIFMATCH)); + } - var key = parseState.GetArgSliceByRef(0).SpanByte; + // SETIFMATCH Args: KEY VAL ETAG -> [ ((EX || PX) expiration)] + int expiry = 0; + ReadOnlySpan errorMessage = default; + var expOption = ExpirationOption.None; + + var tokenIdx = 3; + Span nextOpt = default; + var optUpperCased = false; + while (tokenIdx < parseState.Count || optUpperCased) + { + if (!optUpperCased) + { + nextOpt = parseState.GetArgSliceByRef(tokenIdx++).Span; + } - NetworkSET_Conditional(RespCommand.SETIFMATCH, 0, ref key, getValue: true, highPrecision: false, withEtag: true, ref storageApi); + if (nextOpt.SequenceEqual(CmdStrings.EX)) + { + // Validate expiry + if (!parseState.TryGetInt(tokenIdx++, out expiry)) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + break; + } + + if (expOption != ExpirationOption.None) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; + break; + } + + expOption = ExpirationOption.EX; + if (expiry <= 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET; + break; + } + } + else if (nextOpt.SequenceEqual(CmdStrings.PX)) + { + // Validate expiry + if (!parseState.TryGetInt(tokenIdx++, out expiry)) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + break; + } + + if (expOption != ExpirationOption.None) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; + break; + } + + expOption = ExpirationOption.PX; + if (expiry <= 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET; + break; + } + } + else + { + if (!optUpperCased) + { + AsciiUtils.ToUpperInPlace(nextOpt); + optUpperCased = true; + continue; + } + + errorMessage = CmdStrings.RESP_ERR_GENERIC_UNK_CMD; + break; + } + + optUpperCased = false; + } + + if (!errorMessage.IsEmpty) + { + while (!RespWriteUtils.WriteError(errorMessage, ref dcurr, dend)) + SendAndReset(); + return true; + } + + SpanByte key = parseState.GetArgSliceByRef(0).SpanByte; + + NetworkSET_Conditional(RespCommand.SETIFMATCH, expiry, ref key, getValue: true, highPrecision: expOption == ExpirationOption.PX, withEtag: true, ref storageApi); return true; } @@ -797,7 +882,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref // anything with getValue or withEtag may choose to write to the buffer in success scenarios outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); status = storageApi.SET_Conditional(ref key, - ref input, ref outputBuffer, cmd); + ref input, ref outputBuffer); } else { @@ -809,13 +894,8 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref switch ((getValue, withEtag, cmd, status)) { - case (_, _, RespCommand.SETIFMATCH, GarnetStatus.ETAGMISMATCH): // write back mismatch error - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ETAGMISMTACH, ref dcurr, dend)) - SendAndReset(); - break; - // since SET with etag goes down RMW a not found is okay and data is on buffer - case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND): + case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND): // if getvalue || etag and Status is OK then the response is always on the buffer, getvalue is never used with conditionals // extra pattern matching on command below for invariant get value cannot be used with EXXX and EXNX case (true, _, RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETKEEPTTL, GarnetStatus.OK): diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index af2d1a116a..0b08be5f45 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -292,7 +292,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var cmd = input.header.cmd; int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; - long oldEtag = -1; + long oldEtag = Constants.BaseEtag; if (recordInfo.ETag) { etagIgnoredOffset = Constants.EtagSize; @@ -322,27 +322,37 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re case RespCommand.SETIFMATCH: long etagFromClient = input.parseState.GetLong(1); - // No Etag is the same as having an etag of 0 - long prevEtag = recordInfo.ETag ? *(long*)value.ToPointer() : 0; - - if (prevEtag != etagFromClient) + if (oldEtag != etagFromClient) { - // Cancelling the operation and returning false is used to indicate ETAGMISMATCH - rmwInfo.Action = RMWAction.CancelOperation; - return false; + // write back array of the format [etag, value] + var valueToWrite = value.AsReadOnlySpan(etagIgnoredOffset); + var digitsInLenOfValue = NumUtils.NumDigitsInLong(valueToWrite.Length); + // *2\r\n: + + \r\n + $ + + \r\n + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(oldEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + 1 + digitsInLenOfValue + 2 + valueToWrite.Length + 2, ref valueToWrite, oldEtag, ref output, writeDirect: false); + return true; } // Need Copy update if no space for new value var inputValue = input.parseState.GetArgSliceByRef(0); - if (value.Length < inputValue.length + Constants.EtagSize) + + // retain metadata unless metadata sent + int metadataSize = input.arg1 != 0 ? sizeof(long) : value.MetadataSize; + + if (value.Length < inputValue.length + Constants.EtagSize + metadataSize) return false; + if (input.arg1 != 0) + { + value.ExtraMetadata = input.arg1; + } + recordInfo.SetHasETag(); // Increment the ETag - long newEtag = prevEtag + 1; + long newEtag = oldEtag + 1; // Adjust value length if user shrinks it, how to get rid of spanbyte infront - value.ShrinkSerializedLength(inputValue.Length + Constants.EtagSize); + value.ShrinkSerializedLength(metadataSize + inputValue.Length + Constants.EtagSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); @@ -379,7 +389,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re ArgSlice setValue = input.parseState.GetArgSliceByRef(0); // Need CU if no space for new value - int metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + metadataSize = input.arg1 == 0 ? 0 : sizeof(long); if (setValue.Length + metadataSize > value.Length - nextUpdateEtagOffset) return false; @@ -433,7 +443,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // this is the case where we have withetag option and no etag from before nextUpdateEtagOffset = Constants.EtagSize; nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; - oldEtag = Constants.BaseEtag; } setValue = input.parseState.GetArgSliceByRef(0); @@ -765,20 +774,24 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB long etagToCheckWith = input.parseState.GetLong(1); // lack of an etag is the same as having a zero'd etag long existingEtag; - // No Etag is the same as having an etag of 0 + // No Etag is the same as having the base etag if (rmwInfo.RecordInfo.ETag) { existingEtag = *(long*)oldValue.ToPointer(); } else { - existingEtag = 0; + existingEtag = Constants.BaseEtag; } if (existingEtag != etagToCheckWith) { - // cancellation and return false indicates ETag mismatch - rmwInfo.Action = RMWAction.CancelOperation; + // write back array of the format [etag, value] + var valueToWrite = oldValue.AsReadOnlySpan(etagIgnoredOffset); + var digitsInLenOfValue = NumUtils.NumDigitsInLong(valueToWrite.Length); + // *2\r\n: + + \r\n + $ + + \r\n + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + 1 + digitsInLenOfValue + 2 + valueToWrite.Length + 2, ref valueToWrite, existingEtag, ref output, writeDirect: false); return false; } @@ -848,7 +861,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte bool shouldUpdateEtag = true; int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; - long oldEtag = -1; + long oldEtag = Constants.BaseEtag; if (recordInfo.ETag) { etagIgnoredEnd = oldValue.LengthWithoutMetadata; @@ -861,24 +874,27 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.SETIFMATCH: shouldUpdateEtag = false; - // No Etag is the same as having an etag of 0 - if (!recordInfo.ETag) - { - oldEtag = 0; - } - - *(long*)newValue.ToPointer() = oldEtag + 1; - // Copy input to value Span dest = newValue.AsSpan(Constants.EtagSize); ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - Debug.Assert(src.Length + Constants.EtagSize + oldValue.MetadataSize == newValue.Length); + // retain metadata unless metadata sent + int metadataSize = input.arg1 != 0 ? sizeof(long) : oldValue.MetadataSize; + + Debug.Assert(src.Length + Constants.EtagSize + metadataSize == newValue.Length); - // retain metadata - newValue.ExtraMetadata = oldValue.ExtraMetadata; src.CopyTo(dest); + newValue.ExtraMetadata = oldValue.ExtraMetadata; + if (input.arg1 != 0) + { + newValue.ExtraMetadata = input.arg1; + } + + *(long*)newValue.ToPointer() = oldEtag + 1; + + recordInfo.SetHasETag(); + // Write Etag and Val back to Client CopyRespToWithInput(ref input, ref newValue, ref output, isFromPending: false, 0, -1, hasEtagInVal: true); break; @@ -900,7 +916,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // this is the case where we have withetag option and no etag from before nextUpdateEtagOffset = Constants.EtagSize; nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; - oldEtag = 0; recordInfo.SetHasETag(); } @@ -914,7 +929,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - int metadataSize = input.arg1 == 0 ? 0 : sizeof(long); + metadataSize = input.arg1 == 0 ? 0 : sizeof(long); // new value when allocated should have 8 bytes more if the previous record had etag and the cmd was not SETEXXX Debug.Assert(newInputValue.Length + metadataSize + nextUpdateEtagOffset == newValue.Length); @@ -947,7 +962,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // this is the case where we have withetag option and no etag from before nextUpdateEtagOffset = Constants.EtagSize; nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; - oldEtag = 0; recordInfo.SetHasETag(); } diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 974817a59b..16cbab30c0 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -218,8 +218,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b return sizeof(int) + t.LengthWithoutMetadata; case RespCommand.SETIFMATCH: var newValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; - // always preserves the metadata and includes the etag - return sizeof(int) + newValue.Length + Constants.EtagSize + t.MetadataSize; + int metadataSize = input.arg1 == 0 ? t.MetadataSize : sizeof(long); + return sizeof(int) + newValue.Length + Constants.EtagSize + metadataSize; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: case RespCommand.EXPIREAT: @@ -250,7 +250,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b { var functions = functionsState.GetCustomCommandFunctions((ushort)cmd); // compute metadata for result - var metadataSize = input.arg1 switch + metadataSize = input.arg1 switch { -1 => 0, 0 => t.MetadataSize, diff --git a/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs b/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs index ac0f877754..f09d34e7c9 100644 --- a/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs +++ b/libs/server/Storage/Session/MainStore/HyperLogLogOps.cs @@ -244,7 +244,7 @@ public unsafe GarnetStatus HyperLogLogMerge(ref RawStringInput input, out bool e parseState.InitializeWithArgument(mergeSlice); currInput.parseState = parseState; - SET_Conditional(ref dstKey, ref currInput, ref mergeBuffer, ref currLockableContext, input.header.cmd); + SET_Conditional(ref dstKey, ref currInput, ref mergeBuffer, ref currLockableContext); #endregion } diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index fe4ff370a8..0ec59bb6d9 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -376,7 +376,7 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawSt } } - public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, ref TContext context, RespCommand cmd) + public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawStringInput input, ref SpanByteAndMemory output, ref TContext context) where TContext : ITsavoriteContext { var status = context.RMW(ref key, ref input, ref output); @@ -393,11 +393,6 @@ public unsafe GarnetStatus SET_Conditional(ref SpanByte key, ref RawSt incr_session_notfound(); return GarnetStatus.NOTFOUND; } - else if (cmd == RespCommand.SETIFMATCH && status.IsCanceled) - { - // The RMW operation for SETIFMATCH upon not finding the etags match between the existing record and sent etag returns Cancelled Operation - return GarnetStatus.ETAGMISMATCH; - } else { incr_session_found(); @@ -571,7 +566,6 @@ public unsafe GarnetStatus RENAMENX(ArgSlice oldKeySlice, ArgSlice newKeySlice, return RENAME(oldKeySlice, newKeySlice, storeType, true, out result, withEtag); } - // HK TODO private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, StoreType storeType, bool isNX, out int result, bool withEtag) { RawStringInput input = default; @@ -627,7 +621,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S var expirePtrVal = (byte*)expireMemoryHandle.Pointer; RespReadUtils.TryRead64Int(out var expireTimeMs, ref expirePtrVal, expirePtrVal + expireSpan.Length, out var _); - input = isNX ? new RawStringInput(RespCommand.SETEXNX): new RawStringInput(RespCommand.SET); + input = isNX ? new RawStringInput(RespCommand.SETEXNX) : new RawStringInput(RespCommand.SET); // If the key has an expiration, set the new key with the expiration if (expireTimeMs > 0) diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 5a9a8ed3be..32b5138e97 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -54,14 +54,14 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() IDatabase db = redis.GetDatabase(0); var key = "florida"; - RedisResult res = (RedisResult)db.Execute("SET", [key, "one", "WITHETAG"]); + RedisResult res = db.Execute("SET", [key, "one", "WITHETAG"]); long initalEtag = long.Parse(res.ToString()); ClassicAssert.AreEqual(1, initalEtag); - // ETAGMISMATCH test var incorrectEtag = 1738; - RedisResult etagMismatchMsg = db.Execute("SETIFMATCH", [key, "nextone", incorrectEtag]); - ClassicAssert.AreEqual("ETAGMISMATCH", etagMismatchMsg.ToString()); + RedisResult[] etagMismatchMsg = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", incorrectEtag]); + ClassicAssert.AreEqual("1", etagMismatchMsg[0].ToString()); + ClassicAssert.AreEqual("one", etagMismatchMsg[1].ToString()); // set a bigger val RedisResult[] setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", initalEtag]); @@ -81,8 +81,9 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() ClassicAssert.AreEqual(value, "nextnextone"); // ETAGMISMATCH again - etagMismatchMsg = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]); - ClassicAssert.AreEqual("ETAGMISMATCH", etagMismatchMsg.ToString()); + res = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]); + ClassicAssert.AreEqual(nextEtag.ToString(), res[0].ToString()); + ClassicAssert.AreEqual("nextnextone", res[1].ToString()); // set a smaller val setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "lastOne", nextEtag]); @@ -91,6 +92,122 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() ClassicAssert.AreEqual(4, nextEtag); ClassicAssert.AreEqual(value, "lastOne"); + + // ETAGMISMATCH on data that never had an etag + db.KeyDelete(key); + db.StringSet(key, "one"); + res = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]); + ClassicAssert.AreEqual("0", res[0].ToString()); + ClassicAssert.AreEqual("one", res[1].ToString()); + } + + [Test] + public void SetIfMatchWorksWithExpiration() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + IDatabase db = redis.GetDatabase(0); + + var key = "florida"; + // Scenario: Key existed before and had no expiration + RedisResult res = db.Execute("SET", key, "one", "WITHETAG"); + long initalEtag = long.Parse(res.ToString()); + ClassicAssert.AreEqual(1, initalEtag); + + // expiration added + long updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 1, "EX", 100)[0].ToString()); + ClassicAssert.AreEqual(2, updatedEtagRes); + + // confirm expiration added -> TTL should exist + var ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextoneeexpretained", updatedEtagRes)[0].ToString()); + ClassicAssert.AreEqual(3, updatedEtagRes); + + // TTL should be retained + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + db.KeyDelete(key); // cleanup + + // Scenario: Key existed before and had expiration + res = db.Execute("SET", key, "one", "WITHETAG", "PX", 100000); + + // confirm expiration added -> TTL should exist + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + // change value and retain expiration + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 1)[0].ToString()); + ClassicAssert.AreEqual(2, updatedEtagRes); + + // TTL should be retained + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + // change value and change expiration + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextoneeexpretained", 2, "EX", 100)[0].ToString()); + ClassicAssert.AreEqual(3, updatedEtagRes); + + db.KeyDelete(key); // cleanup + + // Scenario: SET without etag and existing expiration when sent with setifmatch will add etag and retain the expiration too + res = db.Execute("SET", key, "one", "EX", 100000); + // when no etag then count 0 as it's existing etag + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 0)[0].ToString()); + ClassicAssert.AreEqual(1, updatedEtagRes); + + // confirm expiration retained -> TTL should exist + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + // confirm has etag now + var etag = long.Parse(db.Execute("GETWITHETAG", key)[0].ToString()); + ClassicAssert.AreEqual(1, etag); + + db.KeyDelete(key); // cleanup + + + // Scenario: SET without etag and without expiration when sent with setifmatch will add etag and retain the expiration too + // copy update + res = db.Execute("SET", key, "one"); + // when no etag then count 0 as it's existing etag + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "nextone", 0, "EX", 10000)[0].ToString()); + ClassicAssert.AreEqual(1, updatedEtagRes); + + // confirm expiration retained -> TTL should exist + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + // confirm has etag now + etag = long.Parse(db.Execute("GETWITHETAG", key)[0].ToString()); + ClassicAssert.AreEqual(1, etag); + + // same length update + res = db.Execute("SET", key, "one"); + // when no etag then count 0 as it's existing etag + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "two", 0, "EX", 10000)[0].ToString()); + ClassicAssert.AreEqual(1, updatedEtagRes); + + // confirm expiration retained -> TTL should exist + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); + + // confirm has etag now + etag = long.Parse(db.Execute("GETWITHETAG", key)[0].ToString()); + ClassicAssert.AreEqual(1, etag); + + db.KeyDelete(key); // cleanup + + // Scenario: smaller length update + res = db.Execute("SET", key, "oneofusoneofus"); + // when no etag then count 0 as it's existing etag + updatedEtagRes = long.Parse(db.Execute("SETIFMATCH", key, "i", 0, "EX", 10000)[0].ToString()); + ClassicAssert.AreEqual(1, updatedEtagRes); + + // confirm expiration retained -> TTL should exist + ttl = db.KeyTimeToLive(key); + ClassicAssert.IsTrue(ttl.HasValue); } #endregion @@ -338,15 +455,16 @@ public void SETOnAlreadyExistingNonEtagDataOverridesIt() [Test] - public void SetIfMatchOnNonEtagDataReturnsEtagMismatch() + public void SetIfMatchOnNonEtagDataReturnsNewEtagAndValue() { using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); IDatabase db = redis.GetDatabase(0); var _ = db.StringSet("h", "k"); - var res = db.Execute("SETIFMATCH", ["h", "t", "2"]); - ClassicAssert.AreEqual("ETAGMISMATCH", res.ToString()); + var res = (RedisResult[])db.Execute("SETIFMATCH", ["h", "t", "2"]); + ClassicAssert.AreEqual("0", res[0].ToString()); + ClassicAssert.AreEqual("k", res[1].ToString()); } [Test] From 4d38f772323ac23c3691c0df7e19033de2337eb2 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 20 Dec 2024 01:31:32 -0800 Subject: [PATCH 41/87] reuse code smartly --- .../Storage/Functions/MainStore/RMWMethods.cs | 14 ++------------ test/Garnet.test/RespEtagTests.cs | 4 ++-- website/docs/commands/garnet-specific.md | 5 ++--- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 0b08be5f45..97a95edb05 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -324,12 +324,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (oldEtag != etagFromClient) { - // write back array of the format [etag, value] - var valueToWrite = value.AsReadOnlySpan(etagIgnoredOffset); - var digitsInLenOfValue = NumUtils.NumDigitsInLong(valueToWrite.Length); - // *2\r\n: + + \r\n + $ + + \r\n + + \r\n - var numDigitsInEtag = NumUtils.NumDigitsInLong(oldEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + 1 + digitsInLenOfValue + 2 + valueToWrite.Length + 2, ref valueToWrite, oldEtag, ref output, writeDirect: false); + CopyRespToWithInput(ref input, ref value, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: recordInfo.ETag); return true; } @@ -786,12 +781,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (existingEtag != etagToCheckWith) { - // write back array of the format [etag, value] - var valueToWrite = oldValue.AsReadOnlySpan(etagIgnoredOffset); - var digitsInLenOfValue = NumUtils.NumDigitsInLong(valueToWrite.Length); - // *2\r\n: + + \r\n + $ + + \r\n + \r\n - var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + 1 + digitsInLenOfValue + 2 + valueToWrite.Length + 2, ref valueToWrite, existingEtag, ref output, writeDirect: false); + CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); return false; } diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 32b5138e97..75f0e27b4c 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -127,7 +127,7 @@ public void SetIfMatchWorksWithExpiration() // TTL should be retained ttl = db.KeyTimeToLive(key); ClassicAssert.IsTrue(ttl.HasValue); - + db.KeyDelete(key); // cleanup // Scenario: Key existed before and had expiration @@ -207,7 +207,7 @@ public void SetIfMatchWorksWithExpiration() // confirm expiration retained -> TTL should exist ttl = db.KeyTimeToLive(key); - ClassicAssert.IsTrue(ttl.HasValue); + ClassicAssert.IsTrue(ttl.HasValue); } #endregion diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index 07610d8b44..605792a314 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -155,7 +155,7 @@ One of the following: #### **Syntax** ```bash -SETIFMATCH key value etag +SETIFMATCH key value etag [EX || PX expiration] ``` Updates the value of a key if the provided ETag matches the current ETag of the key. @@ -164,9 +164,8 @@ Updates the value of a key if the provided ETag matches the current ETag of the One of the following: -- **Integer reply**: The updated ETag if the value was successfully updated. +- **Array reply**: The updated ETag if the value was successfully updated, and no a - **Nil reply**: If the key does not exist. -- **Simple string reply**: If the provided ETag does not match the current ETag returns VAL_NOT_FOUND --- From fc2d0a088865ebdfcdab211792a5f35d9e6b65f9 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 20 Dec 2024 11:46:48 -0800 Subject: [PATCH 42/87] update documentation --- libs/server/Resp/CmdStrings.cs | 1 - .../Storage/Functions/MainStore/RMWMethods.cs | 19 ++++++++++++----- test/Garnet.test/RespEtagTests.cs | 21 +++++++++---------- website/docs/commands/garnet-specific.md | 16 +++++++++----- website/docs/commands/generic-commands.md | 10 +++++++-- website/docs/commands/raw-string.md | 3 ++- 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index 72288fea79..040bbb234c 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -141,7 +141,6 @@ static partial class CmdStrings public static ReadOnlySpan RESP_PONG => "+PONG\r\n"u8; public static ReadOnlySpan RESP_EMPTY => "$0\r\n\r\n"u8; public static ReadOnlySpan RESP_QUEUED => "+QUEUED\r\n"u8; - public static ReadOnlySpan RESP_ETAGMISMTACH => "+ETAGMISMATCH\r\n"u8; /// /// Simple error response strings, i.e. these are of the form "-errorString\r\n" diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 97a95edb05..61befc2e1c 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -354,8 +354,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re *(long*)value.ToPointer() = newEtag; inputValue.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); - CopyRespToWithInput(ref input, ref value, ref output, false, 0, -1, true); - + // write back array of the format [etag, nil] + var nilResp = CmdStrings.RESP_ERRNOTFOUND; + // *2\r\n: + + \r\n + + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, writeDirect: true); // early return since we already updated the ETag return true; case RespCommand.SET: @@ -881,13 +884,19 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte newValue.ExtraMetadata = input.arg1; } - *(long*)newValue.ToPointer() = oldEtag + 1; + long newEtag = oldEtag + 1; + *(long*)newValue.ToPointer() = newEtag; recordInfo.SetHasETag(); // Write Etag and Val back to Client - CopyRespToWithInput(ref input, ref newValue, ref output, isFromPending: false, 0, -1, hasEtagInVal: true); - break; + // write back array of the format [etag, nil] + var nilResp = CmdStrings.RESP_ERRNOTFOUND; + // *2\r\n: + + \r\n + + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, writeDirect: true); + // early return since we already updated the ETag + return true; case RespCommand.SET: case RespCommand.SETEXXX: diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 75f0e27b4c..7759308d5c 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -67,18 +67,18 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() RedisResult[] setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextone", initalEtag]); long nextEtag = long.Parse(setIfMatchRes[0].ToString()); - string value = setIfMatchRes[1].ToString(); + var value = setIfMatchRes[1]; ClassicAssert.AreEqual(2, nextEtag); - ClassicAssert.AreEqual(value, "nextone"); + ClassicAssert.IsTrue(value.IsNull); // set a bigger val setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "nextnextone", nextEtag]); nextEtag = long.Parse(setIfMatchRes[0].ToString()); - value = setIfMatchRes[1].ToString(); + value = setIfMatchRes[1]; ClassicAssert.AreEqual(3, nextEtag); - ClassicAssert.AreEqual(value, "nextnextone"); + ClassicAssert.IsTrue(value.IsNull); // ETAGMISMATCH again res = db.Execute("SETIFMATCH", [key, "lastOne", incorrectEtag]); @@ -88,10 +88,10 @@ public void SetIfMatchReturnsNewValueAndEtagWhenEtagMatches() // set a smaller val setIfMatchRes = (RedisResult[])db.Execute("SETIFMATCH", [key, "lastOne", nextEtag]); nextEtag = long.Parse(setIfMatchRes[0].ToString()); - value = setIfMatchRes[1].ToString(); + value = setIfMatchRes[1]; ClassicAssert.AreEqual(4, nextEtag); - ClassicAssert.AreEqual(value, "lastOne"); + ClassicAssert.IsTrue(value.IsNull); // ETAGMISMATCH on data that never had an etag db.KeyDelete(key); @@ -340,7 +340,7 @@ public void SETOnAlreadyExistingSETDataOverridesItWithInitialEtag() RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(2, etag); - ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); + ClassicAssert.IsTrue(updateRes[1].IsNull); // inplace update res = db.Execute("SET", "rizz", "meow", "WITHETAG"); @@ -351,12 +351,11 @@ public void SETOnAlreadyExistingSETDataOverridesItWithInitialEtag() updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(4, etag); - ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); + ClassicAssert.IsTrue(updateRes[1].IsNull); // Copy update res = db.Execute("SET", ["rizz", "oneofus", "WITHETAG"]); etag = (long)res; - ClassicAssert.AreEqual(5, etag); // now we should do a getwithetag and see the etag as 0 res = db.Execute("SET", ["rizz", "oneofus"]); @@ -380,7 +379,7 @@ public void SETWithWITHETAGOnAlreadyExistingSETDataOverridesItButUpdatesEtag() RedisResult[] updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fixx", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(2, etag); - ClassicAssert.AreEqual("fixx", updateRes[1].ToString()); + ClassicAssert.IsTrue(updateRes[1].IsNull); // inplace update res = db.Execute("SET", ["rizz", "meow", "WITHETAG"]); @@ -391,7 +390,7 @@ public void SETWithWITHETAGOnAlreadyExistingSETDataOverridesItButUpdatesEtag() updateRes = (RedisResult[])db.Execute("SETIFMATCH", ["rizz", "fooo", etag.ToString()]); etag = (long)updateRes[0]; ClassicAssert.AreEqual(4, etag); - ClassicAssert.AreEqual("fooo", updateRes[1].ToString()); + ClassicAssert.IsTrue(updateRes[1].IsNull); // Copy update res = db.Execute("SET", ["rizz", "oneofus", "WITHETAG"]); diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index 605792a314..54c249ac2a 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -129,6 +129,8 @@ Garnet provides support for ETags on raw strings. By using the ETag-related comm Compatibility with non-ETag commands and the behavior of data inserted with ETags are detailed at the end of this document. +To initialize a key value pair with an ETag you can use either the SET command with the newly added "WITHETAG" optional flag, or you can take any existing Key value pair and call SETIFMATCH with the ETag argument as 0 (Any key value pair without an explicit ETag has an ETag of 0 implicitly). You can read more about setting an initial ETag via SET [here](../commands/raw-string#set) + --- ### **GETWITHETAG** @@ -145,7 +147,7 @@ Retrieves the value and the ETag associated with the given key. One of the following: -- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. If called on a key-value pair without ETag, the first item will be nil. +- **Array reply**: An array of two items returned on success. The first item is an integer representing the ETag, and the second is the bulk string value of the key. If called on a key-value pair without ETag, the etag will be 0. - **Nil reply**: If the key does not exist. --- @@ -155,16 +157,20 @@ One of the following: #### **Syntax** ```bash -SETIFMATCH key value etag [EX || PX expiration] +SETIFMATCH key value etag [EX seconds | PX milliseconds] ``` Updates the value of a key if the provided ETag matches the current ETag of the key. +**Options:** +* EX seconds -- Set the specified expire time, in seconds (a positive integer). +* PX milliseconds -- Set the specified expire time, in milliseconds (a positive integer). + #### **Response** One of the following: -- **Array reply**: The updated ETag if the value was successfully updated, and no a +- **Array reply**: If etags match an array where the first item is the updated etag, and the second value is nil. If the etags do not match the array will hold the latest etag, and the latest value from the in order. - **Nil reply**: If the key does not exist. --- @@ -183,7 +189,7 @@ Retrieves the value if the ETag associated with the key has changed; otherwise, One of the following: -- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the updated ETag, and the second item is the value associated with the key. If called on a record without an ETag the first item in the array will be 0. If the etag matches then we the first item on the array is the existing etag, but the second value is Nil +- **Array reply**: If the ETag does not match, an array of two items is returned. The first item is the latest ETag, and the second item is the value associated with the key. If the Etag matches the first item in the response array is the etag and the second item is nil. - **Nil reply**: If the key does not exist. --- @@ -196,7 +202,7 @@ Below is the expected behavior of ETag-associated key-value pairs when non-ETag - **SET**: Only if used with additional option "WITHETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. -- **RENAME**: Renaming an old ETag-associated key-value pair will create the newly renamed key with an initial etag of 0. If the key being renamed to already existed before hand, it will retain the etag of the existing key that was the target of the rename, and show it as an updated etag. +- **RENAME**: RENAME takes an option for WITHETAG. When called WITHETAG - **Custom Commands**: While etag based key value pairs **can be used blindly inside of custom transactions and custom procedures**, ETag set key value pairs are **not supported to be used from inside of Custom Raw String Functions.** diff --git a/website/docs/commands/generic-commands.md b/website/docs/commands/generic-commands.md index 59ebf9b933..27c8ae6946 100644 --- a/website/docs/commands/generic-commands.md +++ b/website/docs/commands/generic-commands.md @@ -366,11 +366,14 @@ One of the following: #### Syntax ```bash - RENAME key newkey + RENAME key newkey [WITHETAG] ``` Renames key to newkey. It returns an error when key does not exist. If newkey already exists it is overwritten, when this happens RENAME executes an implicit [DEL](#del) operation. +#### **Options:** +* WITHETAG - If the newkey did not exist, the newkey will now have an ETag associated with it after the rename. If the newkey existed before with an ETag the RENAME will update the ETag. If the newkey existed before without an ETag, then after the RENAME the newkey would have an ETag associated with it. You can read more about ETags [here](../commands/garnet-specific-commands#native-etag-support). + #### Resp Reply Simple string reply: OK. @@ -382,11 +385,14 @@ Simple string reply: OK. #### Syntax ```bash - RENAME key newkey + RENAMENX key newkey [WITHETAG] ``` Renames key to newkey if newkey does not yet exist. It returns an error when key does not exist. +#### **Options:** +* WITHETAG - The newkey will now have an ETag associated with it after the rename. You can read more about ETags [here](../commands/garnet-specific-commands#native-etag-support). + #### Resp Reply One of the following: diff --git a/website/docs/commands/raw-string.md b/website/docs/commands/raw-string.md index 00ab1e2f61..1d114bbbbd 100644 --- a/website/docs/commands/raw-string.md +++ b/website/docs/commands/raw-string.md @@ -301,7 +301,7 @@ Set **key** to hold the string value. If key already holds a value, it is overwr * NX -- Only set the key if it does not already exist. * XX -- Only set the key if it already exists. * KEEPTTL -- Retain the time to live associated with the key. -* WITHETAG -- Update the Etag associated with the previous key-value pair, while setting the new value for the key. If no etag existed on the previous key-value pair this will create the new key-value pair without any etag as well. This is a Garnet specific command, you can read more about ETag support [here](../commands/garnet-specific-commands#native-etag-support) +* WITHETAG -- Adding this option sets the Key Value pair with an initial ETag, if called on an existing key value pair with an ETag, this command will update the ETag transparently. This is a Garnet specific command, you can read more about ETag support [here](../commands/garnet-specific-commands#native-etag-support). WITHETAG and GET options cannot be sent at the same time. #### Resp Reply @@ -311,6 +311,7 @@ Any of the following: * Simple string reply: OK. GET not given: The key was set. * Nil reply: GET given: The key didn't exist before the SET. * Bulk string reply: GET given: The previous value of the key. +* Integer reply: WITHETAG given: The ETag either created on the value, or the updated Etag. --- From 8fa17c7df23b2ee0e87448780c01d27e944efc7a Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 20 Dec 2024 13:19:15 -0800 Subject: [PATCH 43/87] Finish documentation --- libs/resources/RespCommandsDocs.json | 126 ++++++++++++++++++ libs/resources/RespCommandsInfo.json | 2 +- libs/server/Resp/BasicCommands.cs | 9 +- .../Implementation/InternalUpsert.cs | 3 - .../Tsavorite/cs/src/core/Utilities/Status.cs | 5 - website/docs/commands/garnet-specific.md | 2 +- 6 files changed, 132 insertions(+), 15 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 74d14f7792..5a0782850f 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -4461,6 +4461,44 @@ } ] }, + { + "Command": "RENAMENX", + "Name": "RENAMENX", + "Summary": "Renames a key and overwrites the destination if the newkey does not exist.", + "Group": "Generic", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "NEWKEY", + "DisplayText": "newkey", + "Type": "Key", + "KeySpecIndex": 1 + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "CONDITION", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHETAG", + "DisplayText": "WITHETAG", + "Type": "PureToken", + "Token": "WITHETAG" + } + ] + } + ] + }, { "Command": "REPLICAOF", "Name": "REPLICAOF", @@ -4888,6 +4926,44 @@ "Token": "GET", "ArgumentFlags": "Optional" }, + { + "Command": "GETIFNOTMATCH", + "Name": "GETIFNOTMATCH", + "Summary": "Gets the ETag and value if the key\u0027s current etag does not match the given etag.", + "Group": "String", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ETAG", + "DisplayText": "etag", + "Type": "Integer" + } + ] + }, + { + "Command": "GETWITHETAG", + "Name": "GETWITHETAG", + "Summary": "Gets the ETag and value for the key", + "Group": "String", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + } + ] + }, { "TypeDiscriminator": "RespCommandContainerArgument", "Name": "EXPIRATION", @@ -4933,6 +5009,56 @@ } ] }, + { + "Command": "SETIFMATCH", + "Name": "SETIFMATCH", + "Summary": "Sets the string value of a key, ignoring its type, if the key\u0027s current etag matches the given etag.", + "Group": "String", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "VALUE", + "DisplayText": "value", + "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ETAG", + "DisplayText": "etag", + "Type": "Integer" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "EXPIRATION", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "SECONDS", + "DisplayText": "seconds", + "Type": "Integer", + "Token": "EX" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MILLISECONDS", + "DisplayText": "milliseconds", + "Type": "Integer", + "Token": "PX" + } + ] + } + ] + }, { "Command": "SETBIT", "Name": "SETBIT", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index c9882467af..e1b4a0b380 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -3507,7 +3507,7 @@ "Command": "SETIFMATCH", "Name": "SETIFMATCH", "IsInternal": false, - "Arity": 4, + "Arity": -4, "Flags": "NONE", "FirstKey": 1, "LastKey": 1, diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 3422e88ebb..8ffaec56e9 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -345,8 +345,7 @@ private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) /// /// GETIFNOTMATCH key etag - /// Given a key and an etag, return the value and it's etag only if the sent ETag does not match the existing ETag - /// If the ETag matches then we just send back a string indicating the value has not changed. + /// Given a key and an etag, return the value and it's etag only if the sent ETag does not match the existing ETag. /// private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi @@ -377,7 +376,7 @@ private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) } /// - /// SETIFNOTMATCH key val etag + /// SETIFMATCH key val etag EX|PX expiry /// Sets a key value pair only if an already existing etag does not match the etag sent as a part of the request /// /// @@ -876,10 +875,10 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref SpanByteAndMemory outputBuffer = default; GarnetStatus status; - // SETIFMATCH will always hit this conditional and have o point to the right memory + // SETIFMATCH will always hit this conditional and have assign output buffer to the right memory location if (getValue || withEtag) { - // anything with getValue or withEtag may choose to write to the buffer in success scenarios + // anything with getValue or withEtag writes to the buffer outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); status = storageApi.SET_Conditional(ref key, ref input, ref outputBuffer); diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs index 53377bd270..7e6c24872a 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Tsavorite/Implementation/InternalUpsert.cs @@ -74,8 +74,6 @@ internal OperationStatus InternalUpsert= hlogBase.ReadOnlyAddress) { srcRecordInfo = ref stackCtx.recSrc.GetInfo(); - srcRecordInfo.ClearHasETag(); // Mutable Region: Update the record in-place. We perform mutable updates only if we are in normal processing phase of checkpointing UpsertInfo upsertInfo = new() diff --git a/libs/storage/Tsavorite/cs/src/core/Utilities/Status.cs b/libs/storage/Tsavorite/cs/src/core/Utilities/Status.cs index 3ecf39eca4..c5acc553fc 100644 --- a/libs/storage/Tsavorite/cs/src/core/Utilities/Status.cs +++ b/libs/storage/Tsavorite/cs/src/core/Utilities/Status.cs @@ -133,11 +133,6 @@ public bool IsCompletedSuccessfully /// public byte Value => (byte)statusCode; - /// - /// Whther the operation performed an update on the record or not - /// - public bool IsUpdated => Record.InPlaceUpdated || Record.CopyUpdated; - /// /// "Found" is zero, so does not appear in the output by default; this handles that explicitly public override string ToString() => (Found ? "Found, " : string.Empty) + statusCode.ToString(); diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index 54c249ac2a..4bec1747bf 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -170,7 +170,7 @@ Updates the value of a key if the provided ETag matches the current ETag of the One of the following: -- **Array reply**: If etags match an array where the first item is the updated etag, and the second value is nil. If the etags do not match the array will hold the latest etag, and the latest value from the in order. +- **Array reply**: If etags match an array where the first item is the updated etag, and the second value is nil. If the etags do not match the array will hold the latest etag, and the latest value in order. - **Nil reply**: If the key does not exist. --- From 6d76b5d5bbaba14a2849e271691d0613313283e3 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sun, 22 Dec 2024 16:21:20 -0800 Subject: [PATCH 44/87] wip --- libs/resources/RespCommandsDocs.json | 114 +++++++++--------- libs/resources/RespCommandsInfo.json | 13 ++ libs/server/Resp/BasicCommands.cs | 18 +-- .../Functions/MainStore/ReadMethods.cs | 60 ++++++++- test/Garnet.test/RespAofTests.cs | 4 +- test/Garnet.test/RespCustomCommandTests.cs | 8 +- test/Garnet.test/TransactionTests.cs | 22 ++-- 7 files changed, 145 insertions(+), 94 deletions(-) diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index 5a0782850f..e7d3999db6 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -4445,19 +4445,12 @@ "KeySpecIndex": 1 }, { - "TypeDiscriminator": "RespCommandKeyArgument", - "Name": "CONDITION", - "Type": "OneOf", + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHETAG", + "DisplayText": "WITHETAG", + "Type": "PureToken", "ArgumentFlags": "Optional", - "Arguments": [ - { - "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "WITHETAG", - "DisplayText": "WITHETAG", - "Type": "PureToken", - "Token": "WITHETAG" - } - ] + "Token": "WITHETAG" } ] }, @@ -4483,19 +4476,12 @@ "KeySpecIndex": 1 }, { - "TypeDiscriminator": "RespCommandKeyArgument", - "Name": "CONDITION", - "Type": "OneOf", + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHETAG", + "DisplayText": "WITHETAG", + "Type": "PureToken", "ArgumentFlags": "Optional", - "Arguments": [ - { - "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "WITHETAG", - "DisplayText": "WITHETAG", - "Type": "PureToken", - "Token": "WITHETAG" - } - ] + "Token": "WITHETAG" } ] }, @@ -4927,42 +4913,12 @@ "ArgumentFlags": "Optional" }, { - "Command": "GETIFNOTMATCH", - "Name": "GETIFNOTMATCH", - "Summary": "Gets the ETag and value if the key\u0027s current etag does not match the given etag.", - "Group": "String", - "Complexity": "O(1)", - "Arguments": [ - { - "TypeDiscriminator": "RespCommandKeyArgument", - "Name": "KEY", - "DisplayText": "key", - "Type": "Key", - "KeySpecIndex": 0 - }, - { - "TypeDiscriminator": "RespCommandBasicArgument", - "Name": "ETAG", - "DisplayText": "etag", - "Type": "Integer" - } - ] - }, - { - "Command": "GETWITHETAG", - "Name": "GETWITHETAG", - "Summary": "Gets the ETag and value for the key", - "Group": "String", - "Complexity": "O(1)", - "Arguments": [ - { - "TypeDiscriminator": "RespCommandKeyArgument", - "Name": "KEY", - "DisplayText": "key", - "Type": "Key", - "KeySpecIndex": 0 - } - ] + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHETAG", + "DisplayText": "WITHETAG", + "Type": "PureToken", + "ArgumentFlags": "Optional", + "Token": "WITHETAG" }, { "TypeDiscriminator": "RespCommandContainerArgument", @@ -5009,6 +4965,44 @@ } ] }, + { + "Command": "GETIFNOTMATCH", + "Name": "GETIFNOTMATCH", + "Summary": "Gets the ETag and value if the key\u0027s current etag does not match the given etag.", + "Group": "String", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ETAG", + "DisplayText": "etag", + "Type": "Integer" + } + ] + }, + { + "Command": "GETWITHETAG", + "Name": "GETWITHETAG", + "Summary": "Gets the ETag and value for the key", + "Group": "String", + "Complexity": "O(1)", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "DisplayText": "key", + "Type": "Key", + "KeySpecIndex": 0 + } + ] + }, { "Command": "SETIFMATCH", "Name": "SETIFMATCH", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index e1b4a0b380..4f258ab0d7 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -3503,6 +3503,19 @@ } ] }, + { + "Command": "SETEXNX", + "Name": "SETEXNX", + "Arity": -3, + "Flags": "NONE", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Fast, String, Write, Transaction", + "Tips": null, + "KeySpecifications": null, + "SubCommands": null + }, { "Command": "SETIFMATCH", "Name": "SETIFMATCH", diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 8ffaec56e9..d560cdd506 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -893,11 +893,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref switch ((getValue, withEtag, cmd, status)) { - // since SET with etag goes down RMW a not found is okay and data is on buffer - case (_, true, RespCommand.SET, GarnetStatus.NOTFOUND): - // if getvalue || etag and Status is OK then the response is always on the buffer, getvalue is never used with conditionals - // extra pattern matching on command below for invariant get value cannot be used with EXXX and EXNX - case (true, _, RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETKEEPTTL, GarnetStatus.OK): + case (true, _, _, GarnetStatus.OK): case (_, true, _, GarnetStatus.OK or GarnetStatus.NOTFOUND): if (!outputBuffer.IsSpanByte) SendAndReset(outputBuffer.Memory, outputBuffer.Length); @@ -906,23 +902,15 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref break; case (false, false, RespCommand.SETEXNX, GarnetStatus.NOTFOUND): // SETEXNX is at success if not found and nothign on buffer if no get or withetag so return +OK - case (false, false, - RespCommand.SET or RespCommand.SETIFMATCH or RespCommand.SETEXXX or RespCommand.SETKEEPTTL or RespCommand.SETKEEPTTLXX, - GarnetStatus.OK): // for everything EXCPET SETEXNX if no get, and no etag, then an OK returns +OK response + case (false, false, not RespCommand.SETEXNX, GarnetStatus.OK): // for everything EXCPET SETEXNX if no get, and no etag, then an OK returns +OK response while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); break; - case (_, _, RespCommand.SETEXNX, GarnetStatus.OK): // For NX semantics an OK indicates a found, which means nothing was set and hence we return NIL - // anything not found that did not come from SETEXNX or WITHETAG always returns NIL, also anything that is indicating wrong type or moved will return NIL - case (_, false, not RespCommand.SETEXNX, GarnetStatus.NOTFOUND or GarnetStatus.WRONGTYPE or GarnetStatus.MOVED): + default: while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); break; - - default: - Debug.Assert(false, $"({getValue}, {withEtag}, {cmd}, {status}) unaccounted for combination in response pattern matching. Please make explicit."); - break; }; return true; diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 0990b3a4ab..e11ac2d298 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -16,16 +16,68 @@ namespace Garnet.server /// public bool SingleReader( ref SpanByte key, ref RawStringInput input, - ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) => Reader(ref key, ref input, ref value, ref dst, ref readInfo, readInfo.RecordInfo.ETag); + ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) + { + var hasEtag = readInfo.RecordInfo.ETag; + if (value.MetadataSize != 0 && CheckExpiry(ref value)) + return false; + var cmd = input.header.cmd; + + var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; + + if (cmd == RespCommand.GETIFNOTMATCH) + { + long etagToMatchAgainst = input.parseState.GetLong(0); + // Any value without an etag is treated the same as a value with an etag + long existingEtag = hasEtag ? *(long*)value.ToPointer() : 0; + if (existingEtag == etagToMatchAgainst) + { + // write back array of the format [etag, nil] + var nilResp = CmdStrings.RESP_ERRNOTFOUND; + // *2\r\n: + + \r\n + + var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, writeDirect: true); + return true; + } + } + else if (cmd > RespCommandExtensions.LastValidCommand) + { + var valueLength = value.LengthWithoutMetadata; + (IMemoryOwner Memory, int Length) output = (dst.Memory, 0); + var ret = functionsState.GetCustomCommandFunctions((ushort)cmd) + .Reader(key.AsReadOnlySpan(), ref input, value.AsReadOnlySpan(), ref output, ref readInfo); + Debug.Assert(valueLength <= value.LengthWithoutMetadata); + dst.Memory = output.Memory; + dst.Length = output.Length; + return ret; + } + + // Unless the command explicitly asks for the ETag in response, we do not write back the ETag + var start = 0; + var end = -1; + if (!isEtagCmd && hasEtag) + { + start = Constants.EtagSize; + end = value.LengthWithoutMetadata; + } + + if (cmd == RespCommand.NONE) + CopyRespTo(ref value, ref dst, start, end); + else + { + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, hasEtag); + } + + return true; + } /// public bool ConcurrentReader( ref SpanByte key, ref RawStringInput input, ref SpanByte value, - ref SpanByteAndMemory dst, ref ReadInfo readInfo, ref RecordInfo recordInfo) => Reader(ref key, ref input, ref value, ref dst, ref readInfo, recordInfo.ETag); - - private bool Reader(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo, bool hasEtag) + ref SpanByteAndMemory dst, ref ReadInfo readInfo, ref RecordInfo recordInfo) { + var hasEtag = readInfo.RecordInfo.ETag; if (value.MetadataSize != 0 && CheckExpiry(ref value)) return false; diff --git a/test/Garnet.test/RespAofTests.cs b/test/Garnet.test/RespAofTests.cs index 5c2842d1f5..e71b5be4b2 100644 --- a/test/Garnet.test/RespAofTests.cs +++ b/test/Garnet.test/RespAofTests.cs @@ -230,7 +230,7 @@ public void AofRMWStoreRecoverTest() db.StringSet("SeAofUpsertRecoverTestKey1", "SeAofUpsertRecoverTestValue1", expiry: TimeSpan.FromDays(1), when: When.NotExists); db.StringSet("SeAofUpsertRecoverTestKey2", "SeAofUpsertRecoverTestValue2", expiry: TimeSpan.FromDays(1), when: When.NotExists); db.Execute("SET", "SeAofUpsertRecoverTestKey3", "SeAofUpsertRecoverTestValue3", "WITHETAG"); - db.Execute("SETIFMATCH", "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", "0"); + db.Execute("SETIFMATCH", "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", "1"); } server.Store.CommitAOF(true); @@ -245,7 +245,7 @@ public void AofRMWStoreRecoverTest() ClassicAssert.AreEqual("SeAofUpsertRecoverTestValue1", recoveredValue.ToString()); recoveredValue = db.StringGet("SeAofUpsertRecoverTestKey2"); ClassicAssert.AreEqual("SeAofUpsertRecoverTestValue2", recoveredValue.ToString()); - ExpectedEtagTest(db, "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", 1); + ExpectedEtagTest(db, "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", 2); } } diff --git a/test/Garnet.test/RespCustomCommandTests.cs b/test/Garnet.test/RespCustomCommandTests.cs index 5b05949e3d..35ad59dc0f 100644 --- a/test/Garnet.test/RespCustomCommandTests.cs +++ b/test/Garnet.test/RespCustomCommandTests.cs @@ -1385,11 +1385,11 @@ public void CustomTxnEtagInteractionTest() // check GETWITHETAG shows updated etag and expected values for both RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", key1); - ClassicAssert.AreEqual("1", res[0].ToString()); + ClassicAssert.AreEqual("2", res[0].ToString()); ClassicAssert.IsTrue(res[1].ToString().All(c => c - 'a' >= 0 && c - 'a' < 26)); res = (RedisResult[])db.Execute("GETWITHETAG", key2); - ClassicAssert.AreEqual("1", res[0].ToString()); + ClassicAssert.AreEqual("2", res[0].ToString()); ClassicAssert.AreEqual("18", res[1].ToString()); } catch (RedisServerException rse) @@ -1425,12 +1425,12 @@ public void CustomProcEtagInteractionTest() // check GETWITHETAG shows updated etag and expected values for both RedisResult[] res = (RedisResult[])db.Execute("GETWITHETAG", key1); // etag not updated for this - ClassicAssert.AreEqual("0", res[0].ToString()); + ClassicAssert.AreEqual("1", res[0].ToString()); ClassicAssert.AreEqual(value1, res[1].ToString()); res = (RedisResult[])db.Execute("GETWITHETAG", key2); // etag updated for this - ClassicAssert.AreEqual("1", res[0].ToString()); + ClassicAssert.AreEqual("2", res[0].ToString()); ClassicAssert.AreEqual("257", res[1].ToString()); } catch (RedisServerException rse) diff --git a/test/Garnet.test/TransactionTests.cs b/test/Garnet.test/TransactionTests.cs index c93bf2ae18..c1ee9d35dc 100644 --- a/test/Garnet.test/TransactionTests.cs +++ b/test/Garnet.test/TransactionTests.cs @@ -225,8 +225,9 @@ public async Task WatchTestWithSetWithEtag() var lightClientRequest = TestUtils.CreateRequest(); byte[] res; - string expectedResponse = ":0\r\n"; + string expectedResponse = ":1\r\n"; res = lightClientRequest.SendCommand("SET key1 value1 WITHETAG"); + var debug = res.AsSpan().Slice(0, expectedResponse.Length).ToArray(); ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); expectedResponse = "+OK\r\n"; @@ -242,7 +243,11 @@ public async Task WatchTestWithSetWithEtag() res = lightClientRequest.SendCommand("SET key2 value2"); ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); - await Task.Run(() => updateKey("key1", "value1_updated", withEtag: true)); + await Task.Run(() => { + using var lightClientRequestCopy = TestUtils.CreateRequest(); + string command = "SET key1 value1_updated WITHETAG"; + lightClientRequestCopy.SendCommand(command); + }); res = lightClientRequest.SendCommand("EXEC"); expectedResponse = "*-1"; @@ -254,22 +259,22 @@ public async Task WatchTestWithSetWithEtag() lightClientRequest.SendCommand("GET key1"); lightClientRequest.SendCommand("SET key2 value2"); // check that all the etag commands can be called inside a transaction - lightClientRequest.SendCommand("SET key3 value2 WITHETAG "); + lightClientRequest.SendCommand("SET key3 value2 WITHETAG"); lightClientRequest.SendCommand("GETWITHETAG key3"); - lightClientRequest.SendCommand("GETIFNOTMATCH key3 0"); - lightClientRequest.SendCommand("SETIFMATCH key3 anotherVal 0"); + lightClientRequest.SendCommand("GETIFNOTMATCH key3 1"); + lightClientRequest.SendCommand("SETIFMATCH key3 anotherVal 1"); lightClientRequest.SendCommand("SET key3 arandomval WITHETAG"); res = lightClientRequest.SendCommand("EXEC"); - expectedResponse = "*7\r\n$14\r\nvalue1_updated\r\n+OK\r\n:0\r\n*2\r\n:0\r\n$6\r\nvalue2\r\n*2\r\n:0\r\n$-1\r\n*2\r\n:1\r\n$10\r\nanotherVal\r\n:2\r\n"; + expectedResponse = "*7\r\n$14\r\nvalue1_updated\r\n+OK\r\n:1\r\n*2\r\n:1\r\n$6\r\nvalue2\r\n*2\r\n:1\r\n$-1\r\n*2\r\n:2\r\n$-1\r\n:3\r\n"; string response = Encoding.ASCII.GetString(res.AsSpan().Slice(0, expectedResponse.Length)); ClassicAssert.AreEqual(expectedResponse, response); // check if we still have the appropriate etag on the key we had set var otherLighClientRequest = TestUtils.CreateRequest(); res = otherLighClientRequest.SendCommand("GETWITHETAG key1"); - expectedResponse = "*2\r\n:1\r\n$14\r\nvalue1_updated\r\n"; + expectedResponse = "*2\r\n:2\r\n$14\r\nvalue1_updated\r\n"; response = Encoding.ASCII.GetString(res.AsSpan().Slice(0, expectedResponse.Length)); ClassicAssert.AreEqual(response, expectedResponse); } @@ -361,11 +366,10 @@ public async Task WatchKeyFromDisk() ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); } - private static void updateKey(string key, string value, bool withEtag = false) + private static void updateKey(string key, string value) { using var lightClientRequest = TestUtils.CreateRequest(); string command = $"SET {key} {value}"; - command += withEtag ? " WITHETAG" : ""; byte[] res = lightClientRequest.SendCommand(command); string expectedResponse = "+OK\r\n"; ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); From 9cb6983666649cf292a7c3a7a1344f8553c5989b Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 23 Dec 2024 01:04:15 -0800 Subject: [PATCH 45/87] branchless programming in hot path --- .../Functions/MainStore/DeleteMethods.cs | 2 + .../Storage/Functions/MainStore/RMWMethods.cs | 82 +++++++++++-------- .../Functions/MainStore/ReadMethods.cs | 35 ++++---- test/Garnet.test/RespEtagTests.cs | 1 - 4 files changed, 64 insertions(+), 56 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/DeleteMethods.cs b/libs/server/Storage/Functions/MainStore/DeleteMethods.cs index d94aa8b7eb..a2bf1fb163 100644 --- a/libs/server/Storage/Functions/MainStore/DeleteMethods.cs +++ b/libs/server/Storage/Functions/MainStore/DeleteMethods.cs @@ -14,6 +14,7 @@ namespace Garnet.server public bool SingleDeleter(ref SpanByte key, ref SpanByte value, ref DeleteInfo deleteInfo, ref RecordInfo recordInfo) { functionsState.watchVersionMap.IncrementVersion(deleteInfo.KeyHash); + recordInfo.ClearHasETag(); return true; } @@ -31,6 +32,7 @@ public bool ConcurrentDeleter(ref SpanByte key, ref SpanByte value, ref DeleteIn functionsState.watchVersionMap.IncrementVersion(deleteInfo.KeyHash); if (functionsState.appendOnlyFile != null) WriteLogDelete(ref key, deleteInfo.Version, deleteInfo.SessionID); + recordInfo.ClearHasETag(); return true; } } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 61befc2e1c..71d1e42790 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using Garnet.common; using Tsavorite.core; @@ -54,7 +55,6 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp /// public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo) { - recordInfo.ClearHasETag(); rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); RespCommand cmd = input.header.cmd; @@ -286,19 +286,23 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (value.MetadataSize > 0 && input.header.CheckExpiry(value.ExtraMetadata)) { rmwInfo.Action = RMWAction.ExpireAndResume; + recordInfo.ClearHasETag(); return false; } - var cmd = input.header.cmd; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; - if (recordInfo.ETag) - { - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = value.LengthWithoutMetadata; - oldEtag = *(long*)value.ToPointer(); - } + RespCommand cmd = input.header.cmd; + + bool hasETag = recordInfo.ETag; + int etagMultiplier = Unsafe.As(ref hasETag); + + // 0 if no etag else EtagSize + int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; + // -1 if no etag or oldValue.LengthWithoutMEtada if etag + int etagIgnoredEnd = etagMultiplier * (value.LengthWithoutMetadata + 1); + etagIgnoredEnd--; + + // 0 if no Etag exists else the first 8 bytes of value + long oldEtag = etagMultiplier * *(long*)value.ToPointer(); switch (cmd) { @@ -747,10 +751,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // increment the Etag transparently if in place update happened - if (recordInfo.ETag && rmwInfo.Action == RMWAction.Default) - { - *(long*)value.ToPointer() = oldEtag + 1; - } + // if etag needs to be updated set frist 8 bytes to oldEtag + 1 or leave it unchanged + long existingDataAtPtr = *(long*)value.ToPointer(); + *(long*)value.ToPointer() = (etagMultiplier * (oldEtag + 1)) + + ((1 - etagMultiplier) * existingDataAtPtr); return true; } @@ -758,13 +762,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - if (rmwInfo.RecordInfo.ETag) - { - etagIgnoredOffset = sizeof(long); - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - } + bool hasETag = rmwInfo.RecordInfo.ETag; + int etagMultiplier = Unsafe.As(ref hasETag); + + // 0 if no etag else EtagSize + int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; + // -1 if no etag or oldValue.LengthWithoutMEtada if etag + int etagIgnoredEnd = etagMultiplier * (oldValue.LengthWithoutMetadata + 1); + etagIgnoredEnd--; switch (input.header.cmd) { @@ -795,6 +800,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (oldValue.MetadataSize > 0 && input.header.CheckExpiry(oldValue.ExtraMetadata)) { rmwInfo.Action = RMWAction.ExpireAndResume; + rmwInfo.RecordInfo.ClearHasETag(); return false; } @@ -852,15 +858,17 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte RespCommand cmd = input.header.cmd; bool shouldUpdateEtag = true; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; - if (recordInfo.ETag) - { - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - etagIgnoredOffset = Constants.EtagSize; - oldEtag = *(long*)oldValue.ToPointer(); - } + + bool hasETag = recordInfo.ETag; + int etagMultiplier = Unsafe.As(ref hasETag); + + // 0 if no etag else EtagSize + int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; + // -1 if no etag else oldValue.LenghWithoutMetadata + int etagIgnoredEnd = etagMultiplier * (oldValue.LengthWithoutMetadata + 1); + etagIgnoredEnd--; + + long oldEtag = etagMultiplier * *(long*)oldValue.ToPointer(); switch (cmd) { @@ -1199,11 +1207,13 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - // increment the Etag transparently if in place update happened - if (recordInfo.ETag && shouldUpdateEtag) - { - *(long*)newValue.ToPointer() = oldEtag + 1; - } + long existingDataAtPtr = *(long*)newValue.ToPointer(); + bool hasEtagAndShouldUpdate = recordInfo.ETag && shouldUpdateEtag; + long hasEtagAndShouldUpdateMultiplier = Unsafe.As(ref hasEtagAndShouldUpdate); + + // if etag needs to be updated set frist 8 bytes to oldEtag + 1 or leave it unchanged + *(long*)newValue.ToPointer() = (hasEtagAndShouldUpdateMultiplier * (oldEtag + 1)) + + ((1 - hasEtagAndShouldUpdateMultiplier) * existingDataAtPtr); return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index e11ac2d298..9b88f5beb8 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using Garnet.common; using Tsavorite.core; @@ -18,19 +19,17 @@ public bool SingleReader( ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) { - var hasEtag = readInfo.RecordInfo.ETag; + bool hasEtag = readInfo.RecordInfo.ETag; if (value.MetadataSize != 0 && CheckExpiry(ref value)) return false; var cmd = input.header.cmd; - var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; - if (cmd == RespCommand.GETIFNOTMATCH) { long etagToMatchAgainst = input.parseState.GetLong(0); // Any value without an etag is treated the same as a value with an etag - long existingEtag = hasEtag ? *(long*)value.ToPointer() : 0; + long existingEtag = Unsafe.As(ref hasEtag) * *(long*)value.ToPointer(); if (existingEtag == etagToMatchAgainst) { // write back array of the format [etag, nil] @@ -54,13 +53,12 @@ public bool SingleReader( } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - var start = 0; - var end = -1; - if (!isEtagCmd && hasEtag) - { - start = Constants.EtagSize; - end = value.LengthWithoutMetadata; - } + bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH) && hasEtag; + int isNotEtagCmdAndRecordHasEtagMultiplier = Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); + + int start = isNotEtagCmdAndRecordHasEtagMultiplier * Constants.EtagSize; + int end = isNotEtagCmdAndRecordHasEtagMultiplier * (value.LengthWithoutMetadata + 1); + end--; if (cmd == RespCommand.NONE) CopyRespTo(ref value, ref dst, start, end); @@ -89,7 +87,7 @@ public bool ConcurrentReader( { long etagToMatchAgainst = input.parseState.GetLong(0); // Any value without an etag is treated the same as a value with an etag - long existingEtag = hasEtag ? *(long*)value.ToPointer() : 0; + long existingEtag = Unsafe.As(ref hasEtag) * *(long*)value.ToPointer(); if (existingEtag == etagToMatchAgainst) { // write back array of the format [etag, nil] @@ -113,13 +111,12 @@ public bool ConcurrentReader( } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - var start = 0; - var end = -1; - if (!isEtagCmd && hasEtag) - { - start = Constants.EtagSize; - end = value.LengthWithoutMetadata; - } + bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH) && hasEtag; + int isNotEtagCmdAndRecordHasEtagMultiplier = Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); + + int start = isNotEtagCmdAndRecordHasEtagMultiplier * Constants.EtagSize; + int end = isNotEtagCmdAndRecordHasEtagMultiplier * (value.LengthWithoutMetadata + 1); + end--; if (cmd == RespCommand.NONE) CopyRespTo(ref value, ref dst, start, end); diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 7759308d5c..109dc865b6 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -653,7 +653,6 @@ public void SetExpiryIncrForEtagSetData() ClassicAssert.AreEqual(1, n); nRetVal = Convert.ToInt64(db.StringGet(strKey)); - ClassicAssert.AreEqual(n, nRetVal); ClassicAssert.AreEqual(1, nRetVal); var etagGet = (RedisResult[])db.Execute("GETWITHETAG", [strKey]); From d764aedb87d5ab202493e920d8920b860ac6640d Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 23 Dec 2024 01:24:10 -0800 Subject: [PATCH 46/87] branchless programming in more hoptpath --- .../Storage/Functions/MainStore/VarLenInputMethods.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 16cbab30c0..f6a7433517 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Runtime.CompilerServices; using Garnet.common; using Tsavorite.core; @@ -143,8 +144,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b if (input.header.cmd != RespCommand.NONE) { var cmd = input.header.cmd; - bool withEtag = input.header.CheckWithEtagFlag(); - int etagOffset = hasEtag || withEtag ? Constants.EtagSize : 0; + bool withEtagOrHasEtag = input.header.CheckWithEtagFlag() || hasEtag; + int etagOffset = Unsafe.As(ref withEtagOrHasEtag) * Constants.EtagSize; switch (cmd) { @@ -205,14 +206,10 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: var setValue = input.parseState.GetArgSliceByRef(0); - if (!withEtag) - etagOffset = 0; return sizeof(int) + t.MetadataSize + setValue.Length + etagOffset; case RespCommand.SET: case RespCommand.SETEXXX: - if (!withEtag) - etagOffset = 0; return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + etagOffset; case RespCommand.PERSIST: return sizeof(int) + t.LengthWithoutMetadata; From ffbce09f4e49c64d5f686c5e7b7724e58650ef1b Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 24 Dec 2024 18:41:58 -0800 Subject: [PATCH 47/87] More branchlessness --- .../Storage/Functions/MainStore/RMWMethods.cs | 18 ++++++--------- .../Functions/MainStore/ReadMethods.cs | 22 +++++++++---------- .../cs/src/core/Index/Common/RecordInfo.cs | 5 +++++ test/Garnet.test/TransactionTests.cs | 3 ++- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 71d1e42790..6b32f0fb9d 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -292,15 +292,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; - bool hasETag = recordInfo.ETag; - int etagMultiplier = Unsafe.As(ref hasETag); + int etagMultiplier = recordInfo.HasETagMultiplier; // 0 if no etag else EtagSize int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; // -1 if no etag or oldValue.LengthWithoutMEtada if etag int etagIgnoredEnd = etagMultiplier * (value.LengthWithoutMetadata + 1); etagIgnoredEnd--; - + // 0 if no Etag exists else the first 8 bytes of value long oldEtag = etagMultiplier * *(long*)value.ToPointer(); @@ -762,8 +761,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { - bool hasETag = rmwInfo.RecordInfo.ETag; - int etagMultiplier = Unsafe.As(ref hasETag); + int etagMultiplier = rmwInfo.RecordInfo.HasETagMultiplier; // 0 if no etag else EtagSize int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; @@ -859,9 +857,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte RespCommand cmd = input.header.cmd; bool shouldUpdateEtag = true; - bool hasETag = recordInfo.ETag; - int etagMultiplier = Unsafe.As(ref hasETag); - + int etagMultiplier = recordInfo.HasETagMultiplier; // 0 if no etag else EtagSize int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; // -1 if no etag else oldValue.LenghWithoutMetadata @@ -910,8 +906,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.SETEXXX: var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - - if (!input.header.CheckWithEtagFlag()) + bool inputWithEtag = input.header.CheckWithEtagFlag(); + if (!inputWithEtag) { // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record nextUpdateEtagOffset = 0; @@ -944,7 +940,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte newValue.ExtraMetadata = input.arg1; newInputValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); - if (input.header.CheckWithEtagFlag()) + if (inputWithEtag) { shouldUpdateEtag = false; *(long*)newValue.ToPointer() = oldEtag + 1; diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 9b88f5beb8..1504d3a48f 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -19,17 +19,18 @@ public bool SingleReader( ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) { - bool hasEtag = readInfo.RecordInfo.ETag; if (value.MetadataSize != 0 && CheckExpiry(ref value)) return false; var cmd = input.header.cmd; + var etagMultiplier = readInfo.RecordInfo.HasETagMultiplier; + if (cmd == RespCommand.GETIFNOTMATCH) { long etagToMatchAgainst = input.parseState.GetLong(0); // Any value without an etag is treated the same as a value with an etag - long existingEtag = Unsafe.As(ref hasEtag) * *(long*)value.ToPointer(); + long existingEtag = etagMultiplier * *(long*)value.ToPointer(); if (existingEtag == etagToMatchAgainst) { // write back array of the format [etag, nil] @@ -53,8 +54,8 @@ public bool SingleReader( } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH) && hasEtag; - int isNotEtagCmdAndRecordHasEtagMultiplier = Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); + bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH); + int isNotEtagCmdAndRecordHasEtagMultiplier = etagMultiplier * Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); int start = isNotEtagCmdAndRecordHasEtagMultiplier * Constants.EtagSize; int end = isNotEtagCmdAndRecordHasEtagMultiplier * (value.LengthWithoutMetadata + 1); @@ -64,7 +65,7 @@ public bool SingleReader( CopyRespTo(ref value, ref dst, start, end); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, hasEtag); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, readInfo.RecordInfo.ETag); } return true; @@ -75,19 +76,18 @@ public bool ConcurrentReader( ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo, ref RecordInfo recordInfo) { - var hasEtag = readInfo.RecordInfo.ETag; if (value.MetadataSize != 0 && CheckExpiry(ref value)) return false; var cmd = input.header.cmd; - var isEtagCmd = cmd is RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH; + var etagMultiplier = readInfo.RecordInfo.HasETagMultiplier; if (cmd == RespCommand.GETIFNOTMATCH) { long etagToMatchAgainst = input.parseState.GetLong(0); // Any value without an etag is treated the same as a value with an etag - long existingEtag = Unsafe.As(ref hasEtag) * *(long*)value.ToPointer(); + long existingEtag = etagMultiplier * *(long*)value.ToPointer(); if (existingEtag == etagToMatchAgainst) { // write back array of the format [etag, nil] @@ -111,8 +111,8 @@ public bool ConcurrentReader( } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH) && hasEtag; - int isNotEtagCmdAndRecordHasEtagMultiplier = Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); + bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH); + int isNotEtagCmdAndRecordHasEtagMultiplier = etagMultiplier * Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); int start = isNotEtagCmdAndRecordHasEtagMultiplier * Constants.EtagSize; int end = isNotEtagCmdAndRecordHasEtagMultiplier * (value.LengthWithoutMetadata + 1); @@ -122,7 +122,7 @@ public bool ConcurrentReader( CopyRespTo(ref value, ref dst, start, end); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, hasEtag); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, readInfo.RecordInfo.ETag); } return true; diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs index 5d82c473f5..7538c97966 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs @@ -283,6 +283,11 @@ public bool ETag public void SetHasETag() => word |= kETagBitMask; public void ClearHasETag() => word &= ~kETagBitMask; + + /// + /// When ETag is set this returns 1 else 0. Used for branchless programming + /// + public int HasETagMultiplier => (int)((word & kETagBitMask) >> kEtagBitOffset); public override readonly string ToString() { diff --git a/test/Garnet.test/TransactionTests.cs b/test/Garnet.test/TransactionTests.cs index c1ee9d35dc..aefd400cdc 100644 --- a/test/Garnet.test/TransactionTests.cs +++ b/test/Garnet.test/TransactionTests.cs @@ -243,7 +243,8 @@ public async Task WatchTestWithSetWithEtag() res = lightClientRequest.SendCommand("SET key2 value2"); ClassicAssert.AreEqual(res.AsSpan().Slice(0, expectedResponse.Length).ToArray(), expectedResponse); - await Task.Run(() => { + await Task.Run(() => + { using var lightClientRequestCopy = TestUtils.CreateRequest(); string command = "SET key1 value1_updated WITHETAG"; lightClientRequestCopy.SendCommand(command); From 7ad569450256bc85073f0a1d18f572ff4f213811 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Dec 2024 12:33:25 -0800 Subject: [PATCH 48/87] reduce branching for set_conditional --- libs/server/Resp/BasicCommands.cs | 63 +++++++++++++++++-------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index d560cdd506..79dcfb7c7f 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -872,48 +872,55 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref if (getValue) input.header.SetSetGetFlag(); - SpanByteAndMemory outputBuffer = default; - GarnetStatus status; - - // SETIFMATCH will always hit this conditional and have assign output buffer to the right memory location if (getValue || withEtag) { - // anything with getValue or withEtag writes to the buffer - outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - status = storageApi.SET_Conditional(ref key, + // anything with getValue or withEtag always writes to the buffer in the happy path + SpanByteAndMemory outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + GarnetStatus status = storageApi.SET_Conditional(ref key, ref input, ref outputBuffer); - } - else - { - // the following debug is the catch any edge case leading to SETIFMATCH skipping the above block - Debug.Assert(cmd != RespCommand.SETIFMATCH, "SETIFMATCH should have gone though pointing to right output variable"); - status = storageApi.SET_Conditional(ref key, ref input); - } + // The data will be on the buffer either when we know the response is ok or when the withEtag flag is set. + bool ok = status != GarnetStatus.NOTFOUND || withEtag; - switch ((getValue, withEtag, cmd, status)) - { - case (true, _, _, GarnetStatus.OK): - case (_, true, _, GarnetStatus.OK or GarnetStatus.NOTFOUND): + if (ok) + { if (!outputBuffer.IsSpanByte) SendAndReset(outputBuffer.Memory, outputBuffer.Length); else dcurr += outputBuffer.Length; - break; + } + else + { + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + } - case (false, false, RespCommand.SETEXNX, GarnetStatus.NOTFOUND): // SETEXNX is at success if not found and nothign on buffer if no get or withetag so return +OK - case (false, false, not RespCommand.SETEXNX, GarnetStatus.OK): // for everything EXCPET SETEXNX if no get, and no etag, then an OK returns +OK response + return true; + } + else + { + // the following debug assertion is the catch any edge case leading to SETIFMATCH skipping the above block + Debug.Assert(cmd != RespCommand.SETIFMATCH, "SETIFMATCH should have gone though pointing to right output variable"); + + GarnetStatus status = storageApi.SET_Conditional(ref key, ref input); + + bool ok = status != GarnetStatus.NOTFOUND; + + if (cmd == RespCommand.SETEXNX) + ok = !ok; + + if (ok) + { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) SendAndReset(); - break; - - default: + } + else + { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); - break; - }; - - return true; + } + return true; + } } /// From 337cab059573ea34a882914d569794148a500d62 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Dec 2024 22:55:56 -0800 Subject: [PATCH 49/87] fmt --- libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs index 7538c97966..ade83cc984 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs @@ -283,7 +283,7 @@ public bool ETag public void SetHasETag() => word |= kETagBitMask; public void ClearHasETag() => word &= ~kETagBitMask; - + /// /// When ETag is set this returns 1 else 0. Used for branchless programming /// From 70f09b3e9a3224f4dd4a31686b5f78e945b32670 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 25 Dec 2024 23:34:04 -0800 Subject: [PATCH 50/87] experiment with calling the accessor only once --- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 6b32f0fb9d..ab9aa0ee46 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -372,7 +372,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!input.header.CheckWithEtagFlag()) + bool inputHeaderHasEtag = input.header.CheckWithEtagFlag(); + if (!inputHeaderHasEtag) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record nextUpdateEtagOffset = 0; @@ -402,7 +403,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - if (input.header.CheckWithEtagFlag()) + if (inputHeaderHasEtag) recordInfo.SetHasETag(); // Adjust value length @@ -415,7 +416,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(nextUpdateEtagOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (input.header.CheckWithEtagFlag()) + if (inputHeaderHasEtag) { *(long*)value.ToPointer() = oldEtag + 1; // withetag flag means we need to write etag back to the output buffer From 63e80f8e0be604af9f553d3460b23521969774fb Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 26 Dec 2024 00:13:11 -0800 Subject: [PATCH 51/87] more branchless --- libs/server/InputHeader.cs | 14 ++++++++------ .../Storage/Functions/MainStore/RMWMethods.cs | 4 ++-- .../Functions/MainStore/VarLenInputMethods.cs | 10 ++++++---- .../Storage/Session/MainStore/MainStoreOps.cs | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index 58f1004577..bd7267c555 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -15,6 +15,12 @@ namespace Garnet.server [Flags] public enum RespInputFlags : byte { + /// + /// Flag indicating if a SET operation should either add an etag or respect the etag semantics for a value with an etag already + /// This is used for conditional setting. + /// + WithEtag = 1, + /// /// Flag indicating a SET operation that returns the previous value /// @@ -27,12 +33,6 @@ public enum RespInputFlags : byte /// Expired /// Expired = 128, - - /// - /// Flag indicating if a SET operation should either add an etag or respect the etag semantics for a value with an etag already - /// This is used for conditional setting. - /// - WithEtag = 129, } /// @@ -140,6 +140,8 @@ internal ListOperation ListOp /// internal unsafe bool CheckWithEtagFlag() => (flags & RespInputFlags.WithEtag) != 0; + internal int CheckWithEtagFlagMultiplier => (int)(flags & RespInputFlags.WithEtag); + /// /// Check if record is expired, either deterministically during log replay, /// or based on current time in normal operation. diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index ab9aa0ee46..86bdefef24 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -83,10 +83,10 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: bool withEtag = input.header.CheckWithEtagFlag(); - int spaceForEtag = 0; + int spaceForEtag = input.header.CheckWithEtagFlagMultiplier * Constants.EtagSize; + if (withEtag) { - spaceForEtag = Constants.EtagSize; recordInfo.SetHasETag(); } diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index f6a7433517..649c8ca7bb 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -69,7 +69,6 @@ static bool IsValidDouble(int length, byte* source, out double val) /// public int GetRMWInitialValueLength(ref RawStringInput input) { - var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); var cmd = input.header.cmd; switch (cmd) @@ -120,6 +119,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + ndigits; default: + var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); if (cmd > RespCommandExtensions.LastValidCommand) { var functions = functionsState.GetCustomCommandFunctions((ushort)cmd); @@ -133,7 +133,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - int allocationForEtag = input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0; + int allocationForEtag = input.header.CheckWithEtagFlagMultiplier * Constants.EtagSize; return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize + allocationForEtag; } } @@ -144,8 +144,10 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b if (input.header.cmd != RespCommand.NONE) { var cmd = input.header.cmd; - bool withEtagOrHasEtag = input.header.CheckWithEtagFlag() || hasEtag; - int etagOffset = Unsafe.As(ref withEtagOrHasEtag) * Constants.EtagSize; + // Branchless OR condition on A && B can be expressed as => 1-(1−A)⋅(1−B) + int withEtagOrHasEtag = 1 - (1 - input.header.CheckWithEtagFlagMultiplier) * (1 - Unsafe.As(ref hasEtag)); + + int etagOffset = withEtagOrHasEtag * Constants.EtagSize; switch (cmd) { diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index 0ec59bb6d9..a8586c3e57 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -638,7 +638,7 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; if (withEtag) - input.header.CheckWithEtagFlag(); + input.header.SetWithEtagFlag(); var setStatus = SET_Conditional(ref newKey, ref input, ref context); From 1e4e256f293c1c76cff5740d03236b30a1e00fc2 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 26 Dec 2024 01:14:55 -0800 Subject: [PATCH 52/87] reduce branchless because branchless has more instructions --- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 5 +++-- .../Storage/Functions/MainStore/VarLenInputMethods.cs | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 86bdefef24..f4d424194f 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -83,13 +83,14 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: bool withEtag = input.header.CheckWithEtagFlag(); - int spaceForEtag = input.header.CheckWithEtagFlagMultiplier * Constants.EtagSize; - + int spaceForEtag = 0; if (withEtag) { + spaceForEtag = Constants.EtagSize; recordInfo.SetHasETag(); } + // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 649c8ca7bb..a4d21b821a 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -144,10 +144,12 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, b if (input.header.cmd != RespCommand.NONE) { var cmd = input.header.cmd; - // Branchless OR condition on A && B can be expressed as => 1-(1−A)⋅(1−B) - int withEtagOrHasEtag = 1 - (1 - input.header.CheckWithEtagFlagMultiplier) * (1 - Unsafe.As(ref hasEtag)); - int etagOffset = withEtagOrHasEtag * Constants.EtagSize; + int etagOffset = 0; + if (hasEtag || input.header.CheckWithEtagFlag()) + { + etagOffset = Constants.EtagSize; + } switch (cmd) { From 73cebec1234ee8d061aa4c5d11884af8b3802031 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 26 Dec 2024 01:46:37 -0800 Subject: [PATCH 53/87] reduce branching --- .../Storage/Functions/MainStore/RMWMethods.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index f4d424194f..de4d207a11 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -752,10 +752,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // increment the Etag transparently if in place update happened - // if etag needs to be updated set frist 8 bytes to oldEtag + 1 or leave it unchanged - long existingDataAtPtr = *(long*)value.ToPointer(); - *(long*)value.ToPointer() = (etagMultiplier * (oldEtag + 1)) + - ((1 - etagMultiplier) * existingDataAtPtr); + if (recordInfo.ETag) + { + *(long*)value.ToPointer() = oldEtag + 1; + } return true; } @@ -1205,13 +1205,10 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - long existingDataAtPtr = *(long*)newValue.ToPointer(); - bool hasEtagAndShouldUpdate = recordInfo.ETag && shouldUpdateEtag; - long hasEtagAndShouldUpdateMultiplier = Unsafe.As(ref hasEtagAndShouldUpdate); - - // if etag needs to be updated set frist 8 bytes to oldEtag + 1 or leave it unchanged - *(long*)newValue.ToPointer() = (hasEtagAndShouldUpdateMultiplier * (oldEtag + 1)) + - ((1 - hasEtagAndShouldUpdateMultiplier) * existingDataAtPtr); + if (recordInfo.ETag && shouldUpdateEtag) + { + *(long*)newValue.ToPointer() = oldEtag + 1; + } return true; } From 764b85fd4a64d2b9d55fb3659294ce8d52b83e09 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 26 Dec 2024 13:21:50 -0800 Subject: [PATCH 54/87] remoove reinterpret casting more --- .../Functions/MainStore/ReadMethods.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 1504d3a48f..cd205f9745 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -53,13 +53,15 @@ public bool SingleReader( return ret; } + int start = 0; + int end = -1; // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH); - int isNotEtagCmdAndRecordHasEtagMultiplier = etagMultiplier * Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); + if (cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + { + start = Constants.EtagSize; + end = value.LengthWithoutMetadata; + } - int start = isNotEtagCmdAndRecordHasEtagMultiplier * Constants.EtagSize; - int end = isNotEtagCmdAndRecordHasEtagMultiplier * (value.LengthWithoutMetadata + 1); - end--; if (cmd == RespCommand.NONE) CopyRespTo(ref value, ref dst, start, end); @@ -110,13 +112,14 @@ public bool ConcurrentReader( return ret; } + int start = 0; + int end = -1; // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - bool isNotEtagCmdAndRecordHasEtag = cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH); - int isNotEtagCmdAndRecordHasEtagMultiplier = etagMultiplier * Unsafe.As(ref isNotEtagCmdAndRecordHasEtag); - - int start = isNotEtagCmdAndRecordHasEtagMultiplier * Constants.EtagSize; - int end = isNotEtagCmdAndRecordHasEtagMultiplier * (value.LengthWithoutMetadata + 1); - end--; + if (cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + { + start = Constants.EtagSize; + end = value.LengthWithoutMetadata; + } if (cmd == RespCommand.NONE) CopyRespTo(ref value, ref dst, start, end); From 58094592fe0d2c028df326faaa168dbedc53b571 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 26 Dec 2024 16:23:51 -0800 Subject: [PATCH 55/87] fix read --- libs/server/Storage/Functions/MainStore/ReadMethods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index cd205f9745..4527876451 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -56,7 +56,7 @@ public bool SingleReader( int start = 0; int end = -1; // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - if (cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + if (readInfo.RecordInfo.ETag && cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { start = Constants.EtagSize; end = value.LengthWithoutMetadata; @@ -115,7 +115,7 @@ public bool ConcurrentReader( int start = 0; int end = -1; // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - if (cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + if (recordInfo.ETag && cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { start = Constants.EtagSize; end = value.LengthWithoutMetadata; From 9bc570b314a45fa8f6bc128422646ceba5f69893 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 27 Dec 2024 19:29:31 -0800 Subject: [PATCH 56/87] precompute offsets --- libs/server/InputHeader.cs | 6 +++ libs/server/Resp/BasicCommands.cs | 3 ++ libs/server/Storage/EtagOffsetManagement.cs | 41 ++++++++++++++++ .../Functions/MainStore/DeleteMethods.cs | 2 - .../Storage/Functions/MainStore/RMWMethods.cs | 49 +++++-------------- .../Functions/MainStore/VarLenInputMethods.cs | 10 ++-- .../ObjectStore/VarLenInputMethods.cs | 2 +- .../cs/src/core/Allocator/AllocatorScan.cs | 2 +- .../core/Allocator/SpanByteAllocatorImpl.cs | 2 +- .../ClientSession/SessionFunctionsWrapper.cs | 2 +- .../core/Compaction/LogCompactionFunctions.cs | 2 +- .../Index/Interfaces/ISessionFunctions.cs | 2 +- .../Index/Interfaces/SessionFunctionsBase.cs | 2 +- .../src/core/VarLen/IVariableLengthInput.cs | 2 +- .../cs/src/core/VarLen/SpanByteFunctions.cs | 2 +- .../Tsavorite/cs/test/ExpirationTests.cs | 2 +- .../Tsavorite/cs/test/RevivificationTests.cs | 2 +- 17 files changed, 77 insertions(+), 56 deletions(-) create mode 100644 libs/server/Storage/EtagOffsetManagement.cs diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index bd7267c555..400b6ea191 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -319,6 +319,12 @@ public unsafe int DeserializeFrom(byte* src) /// public struct RawStringInput : IStoreInput { + /// + /// Mutable state we keep around for efficient EtagOffsetManagement, this will be removed when ETag is stored at the record level separately and does not require offset management. + /// NOTE: We do not serialize this to disk or read it from disk, it is only used in memory, and a temporary solution to keep track of Etag offsets across method calls. + /// + public EtagOffsetManagementContext etagOffsetManagementContext; + /// /// Common input header for Garnet /// diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 79dcfb7c7f..626a2c46f3 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -867,7 +867,10 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref var input = new RawStringInput(cmd, ref parseState, startIdx: 1, arg1: inputArg); if (withEtag) + { input.header.SetWithEtagFlag(); + input.etagOffsetManagementContext = input.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(); + } if (getValue) input.header.SetSetGetFlag(); diff --git a/libs/server/Storage/EtagOffsetManagement.cs b/libs/server/Storage/EtagOffsetManagement.cs new file mode 100644 index 0000000000..971a5a9d99 --- /dev/null +++ b/libs/server/Storage/EtagOffsetManagement.cs @@ -0,0 +1,41 @@ +using System.Threading; +using Tsavorite.core; + +namespace Garnet.server +{ + public struct EtagOffsetManagementContext + { + // default values for when no Etag exists on a record + public int EtagIgnoredOffset { get; private set; } + + public int EtagIgnoredEnd { get; private set; } + + public long ExistingEtag { get; private set; } + + public int EtagOffsetBasedOnInputHeaderOrRecordInfo { get; private set; } + + public EtagOffsetManagementContext SetEtagOffsetBasedOnInputHeader() + { + EtagOffsetBasedOnInputHeaderOrRecordInfo = Constants.EtagSize; + return this; + } + + public unsafe EtagOffsetManagementContext CalculateOffsets(bool hasEtag, ref SpanByte value) + { + if (hasEtag) + { + EtagOffsetBasedOnInputHeaderOrRecordInfo = EtagIgnoredOffset = Constants.EtagSize; + EtagIgnoredEnd = value.LengthWithoutMetadata; + ExistingEtag = *(long*)value.ToPointer(); + } + else + { + EtagIgnoredOffset = 0; + EtagIgnoredEnd = -1; + ExistingEtag = Constants.BaseEtag; + } + + return this; + } + } +} \ No newline at end of file diff --git a/libs/server/Storage/Functions/MainStore/DeleteMethods.cs b/libs/server/Storage/Functions/MainStore/DeleteMethods.cs index a2bf1fb163..d94aa8b7eb 100644 --- a/libs/server/Storage/Functions/MainStore/DeleteMethods.cs +++ b/libs/server/Storage/Functions/MainStore/DeleteMethods.cs @@ -14,7 +14,6 @@ namespace Garnet.server public bool SingleDeleter(ref SpanByte key, ref SpanByte value, ref DeleteInfo deleteInfo, ref RecordInfo recordInfo) { functionsState.watchVersionMap.IncrementVersion(deleteInfo.KeyHash); - recordInfo.ClearHasETag(); return true; } @@ -32,7 +31,6 @@ public bool ConcurrentDeleter(ref SpanByte key, ref SpanByte value, ref DeleteIn functionsState.watchVersionMap.IncrementVersion(deleteInfo.KeyHash); if (functionsState.appendOnlyFile != null) WriteLogDelete(ref key, deleteInfo.Version, deleteInfo.SessionID); - recordInfo.ClearHasETag(); return true; } } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index de4d207a11..5dd891ba1d 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -4,7 +4,6 @@ using System; using System.Buffers; using System.Diagnostics; -using System.Runtime.CompilerServices; using Garnet.common; using Tsavorite.core; @@ -293,16 +292,12 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; - int etagMultiplier = recordInfo.HasETagMultiplier; + // Copy Inplace Update worker is the first in the potential pipeline of calling NeedCopyUpdate and CopyUpdater the following line will keep a precomputed values to use after this + input.etagOffsetManagementContext = input.etagOffsetManagementContext.CalculateOffsets(recordInfo.ETag, ref value); - // 0 if no etag else EtagSize - int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; - // -1 if no etag or oldValue.LengthWithoutMEtada if etag - int etagIgnoredEnd = etagMultiplier * (value.LengthWithoutMetadata + 1); - etagIgnoredEnd--; - - // 0 if no Etag exists else the first 8 bytes of value - long oldEtag = etagMultiplier * *(long*)value.ToPointer(); + int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; + long oldEtag = input.etagOffsetManagementContext.ExistingEtag; switch (cmd) { @@ -763,29 +758,15 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { - int etagMultiplier = rmwInfo.RecordInfo.HasETagMultiplier; - - // 0 if no etag else EtagSize - int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; - // -1 if no etag or oldValue.LengthWithoutMEtada if etag - int etagIgnoredEnd = etagMultiplier * (oldValue.LengthWithoutMetadata + 1); - etagIgnoredEnd--; + // offsets are precomputed at inplace updater + int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; + long existingEtag = input.etagOffsetManagementContext.ExistingEtag; switch (input.header.cmd) { case RespCommand.SETIFMATCH: long etagToCheckWith = input.parseState.GetLong(1); - // lack of an etag is the same as having a zero'd etag - long existingEtag; - // No Etag is the same as having the base etag - if (rmwInfo.RecordInfo.ETag) - { - existingEtag = *(long*)oldValue.ToPointer(); - } - else - { - existingEtag = Constants.BaseEtag; - } if (existingEtag != etagToCheckWith) { @@ -859,14 +840,10 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte RespCommand cmd = input.header.cmd; bool shouldUpdateEtag = true; - int etagMultiplier = recordInfo.HasETagMultiplier; - // 0 if no etag else EtagSize - int etagIgnoredOffset = etagMultiplier * Constants.EtagSize; - // -1 if no etag else oldValue.LenghWithoutMetadata - int etagIgnoredEnd = etagMultiplier * (oldValue.LengthWithoutMetadata + 1); - etagIgnoredEnd--; - - long oldEtag = etagMultiplier * *(long*)oldValue.ToPointer(); + // offsets are precomputed at InPlaceUpdater + int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; + long oldEtag = input.etagOffsetManagementContext.ExistingEtag; switch (cmd) { diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index a4d21b821a..dca72a6a84 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System.Runtime.CompilerServices; using Garnet.common; using Tsavorite.core; @@ -139,17 +138,14 @@ public int GetRMWInitialValueLength(ref RawStringInput input) } /// - public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input, bool hasEtag) + public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) { if (input.header.cmd != RespCommand.NONE) { var cmd = input.header.cmd; - int etagOffset = 0; - if (hasEtag || input.header.CheckWithEtagFlag()) - { - etagOffset = Constants.EtagSize; - } + // use the precomputed value + int etagOffset = input.etagOffsetManagementContext.EtagOffsetBasedOnInputHeaderOrRecordInfo; switch (cmd) { diff --git a/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs index 6227bb272c..63af04149b 100644 --- a/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/ObjectStore/VarLenInputMethods.cs @@ -12,7 +12,7 @@ namespace Garnet.server public readonly unsafe partial struct ObjectSessionFunctions : ISessionFunctions { /// - public int GetRMWModifiedValueLength(ref IGarnetObject value, ref ObjectInput input, bool hasEtag) + public int GetRMWModifiedValueLength(ref IGarnetObject value, ref ObjectInput input) { throw new GarnetException("GetRMWModifiedValueLength is not available on the object store"); } diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs index 25e293d410..fc732747b3 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorScan.cs @@ -346,7 +346,7 @@ public void PostInitialUpdater(ref TKey key, ref TInput input, ref TValue value, public void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput output, Empty ctx, Status status, RecordMetadata recordMetadata) { } - public int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag) => 0; + public int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => 0; public int GetRMWInitialValueLength(ref TInput input) => 0; public int GetUpsertValueLength(ref TValue value, ref TInput input) => 0; diff --git a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs index b5feea4dd9..f01495c1fd 100644 --- a/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs +++ b/libs/storage/Tsavorite/cs/src/core/Allocator/SpanByteAllocatorImpl.cs @@ -127,7 +127,7 @@ public ref SpanByte GetAndInitializeValue(long physicalAddress, long endAddress) { // Used by RMW to determine the length of copy destination (taking Input into account), so does not need to get filler length. var keySize = key.TotalSize; - var size = RecordInfo.GetLength() + RoundUp(keySize, Constants.kRecordAlignment) + varlenInput.GetRMWModifiedValueLength(ref value, ref input, recordInfo.ETag); + var size = RecordInfo.GetLength() + RoundUp(keySize, Constants.kRecordAlignment) + varlenInput.GetRMWModifiedValueLength(ref value, ref input); return (size, RoundUp(size, Constants.kRecordAlignment), keySize); } diff --git a/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs b/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs index bd8f2a38bb..9f3eb13825 100644 --- a/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs +++ b/libs/storage/Tsavorite/cs/src/core/ClientSession/SessionFunctionsWrapper.cs @@ -195,7 +195,7 @@ public void UnlockTransientShared(ref TKey key, ref OperationStackContext _clientSession.functions.GetRMWInitialValueLength(ref input); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetRMWModifiedValueLength(ref TValue t, ref TInput input, bool hasEtag) => _clientSession.functions.GetRMWModifiedValueLength(ref t, ref input, hasEtag); + public int GetRMWModifiedValueLength(ref TValue t, ref TInput input) => _clientSession.functions.GetRMWModifiedValueLength(ref t, ref input); [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetUpsertValueLength(ref TValue t, ref TInput input) => _clientSession.functions.GetUpsertValueLength(ref t, ref input); diff --git a/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs b/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs index 2d7ddee19a..4c7e79f8cf 100644 --- a/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/Compaction/LogCompactionFunctions.cs @@ -50,7 +50,7 @@ public void ReadCompletionCallback(ref TKey key, ref TInput input, ref TOutput o public void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput output, TContext ctx, Status status, RecordMetadata recordMetadata) { } - public int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag) => 0; + public int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => 0; public int GetRMWInitialValueLength(ref TInput input) => 0; public int GetUpsertValueLength(ref TValue value, ref TInput input) => _functions.GetUpsertValueLength(ref value, ref input); diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs index 18c01c4c17..f9c3990984 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/ISessionFunctions.cs @@ -183,7 +183,7 @@ public interface ISessionFunctions /// /// Length of resulting value object when performing RMW modification of value using given input /// - int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag); + int GetRMWModifiedValueLength(ref TValue value, ref TInput input); /// /// Initial expected length of value object when populated by RMW using given input diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs index 77ee2dbf83..2d222e4b33 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Interfaces/SessionFunctionsBase.cs @@ -54,7 +54,7 @@ public virtual void ReadCompletionCallback(ref TKey key, ref TInput input, ref T public virtual void RMWCompletionCallback(ref TKey key, ref TInput input, ref TOutput output, TContext ctx, Status status, RecordMetadata recordMetadata) { } /// - public virtual int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag) => throw new TsavoriteException("GetRMWModifiedValueLength is only available for SpanByte Functions"); + public virtual int GetRMWModifiedValueLength(ref TValue value, ref TInput input) => throw new TsavoriteException("GetRMWModifiedValueLength is only available for SpanByte Functions"); /// public virtual int GetRMWInitialValueLength(ref TInput input) => throw new TsavoriteException("GetRMWInitialValueLength is only available for SpanByte Functions"); /// diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs index d67f5f5d48..121a051d53 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/IVariableLengthInput.cs @@ -11,7 +11,7 @@ public interface IVariableLengthInput /// /// Length of resulting value object when performing RMW modification of value using given input /// - int GetRMWModifiedValueLength(ref TValue value, ref TInput input, bool hasEtag); + int GetRMWModifiedValueLength(ref TValue value, ref TInput input); /// /// Initial expected length of value object when populated by RMW using given input diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs index dec7b6ae12..d9a8625b3e 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByteFunctions.cs @@ -119,7 +119,7 @@ public override bool InPlaceUpdater(ref SpanByte key, ref SpanByte input, ref Sp /// Length of resulting object when doing RMW with given value and input. Here we set the length /// to the max of input and old value lengths. You can provide a custom implementation for other cases. /// - public override int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input, bool hasEtag) + public override int GetRMWModifiedValueLength(ref SpanByte t, ref SpanByte input) => sizeof(int) + (t.Length > input.Length ? t.Length : input.Length); /// diff --git a/libs/storage/Tsavorite/cs/test/ExpirationTests.cs b/libs/storage/Tsavorite/cs/test/ExpirationTests.cs index 0df92a2656..04adfc9bae 100644 --- a/libs/storage/Tsavorite/cs/test/ExpirationTests.cs +++ b/libs/storage/Tsavorite/cs/test/ExpirationTests.cs @@ -470,7 +470,7 @@ public override void ReadCompletionCallback(ref SpanByte key, ref ExpirationInpu } /// - public override int GetRMWModifiedValueLength(ref SpanByte value, ref ExpirationInput input, bool hasEtag) => value.TotalSize; + public override int GetRMWModifiedValueLength(ref SpanByte value, ref ExpirationInput input) => value.TotalSize; /// public override int GetRMWInitialValueLength(ref ExpirationInput input) => MinValueLen; diff --git a/libs/storage/Tsavorite/cs/test/RevivificationTests.cs b/libs/storage/Tsavorite/cs/test/RevivificationTests.cs index d10cd125bc..4ec87cdb82 100644 --- a/libs/storage/Tsavorite/cs/test/RevivificationTests.cs +++ b/libs/storage/Tsavorite/cs/test/RevivificationTests.cs @@ -576,7 +576,7 @@ public override bool InPlaceUpdater(ref SpanByte key, ref SpanByte input, ref Sp } // Override the default SpanByteFunctions impelementation; for these tests, we always want the input length. - public override int GetRMWModifiedValueLength(ref SpanByte value, ref SpanByte input, bool hasEtag) => input.TotalSize; + public override int GetRMWModifiedValueLength(ref SpanByte value, ref SpanByte input) => input.TotalSize; public override bool SingleDeleter(ref SpanByte key, ref SpanByte value, ref DeleteInfo deleteInfo, ref RecordInfo recordInfo) { From 458e2e88a348150548b9c70a481cea0870bd4960 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sat, 28 Dec 2024 02:40:05 -0800 Subject: [PATCH 57/87] Fix bugs --- libs/server/AOF/AofProcessor.cs | 3 +++ libs/server/Resp/BasicCommands.cs | 2 +- libs/server/Storage/EtagOffsetManagement.cs | 24 +++++++++---------- .../Storage/Functions/MainStore/RMWMethods.cs | 2 +- .../Functions/MainStore/VarLenInputMethods.cs | 5 ++-- .../Storage/Session/MainStore/MainStoreOps.cs | 6 +++++ 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 94bf35913c..acf152b6b8 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -320,6 +320,9 @@ static unsafe void StoreRMW(BasicContext(RespCommand cmd, int expiry, ref if (withEtag) { input.header.SetWithEtagFlag(); - input.etagOffsetManagementContext = input.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(); + EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); } if (getValue) diff --git a/libs/server/Storage/EtagOffsetManagement.cs b/libs/server/Storage/EtagOffsetManagement.cs index 971a5a9d99..75b4f853f7 100644 --- a/libs/server/Storage/EtagOffsetManagement.cs +++ b/libs/server/Storage/EtagOffsetManagement.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Threading; using Tsavorite.core; @@ -12,30 +13,27 @@ public struct EtagOffsetManagementContext public long ExistingEtag { get; private set; } - public int EtagOffsetBasedOnInputHeaderOrRecordInfo { get; private set; } + public int EtagOffsetForVarlen { get; private set; } - public EtagOffsetManagementContext SetEtagOffsetBasedOnInputHeader() + public static void SetEtagOffsetBasedOnInputHeader(ref EtagOffsetManagementContext context) { - EtagOffsetBasedOnInputHeaderOrRecordInfo = Constants.EtagSize; - return this; + context.EtagOffsetForVarlen = Constants.EtagSize; } - public unsafe EtagOffsetManagementContext CalculateOffsets(bool hasEtag, ref SpanByte value) + public static unsafe void CalculateOffsets(ref EtagOffsetManagementContext context, bool hasEtag, ref SpanByte value) { if (hasEtag) { - EtagOffsetBasedOnInputHeaderOrRecordInfo = EtagIgnoredOffset = Constants.EtagSize; - EtagIgnoredEnd = value.LengthWithoutMetadata; - ExistingEtag = *(long*)value.ToPointer(); + context.EtagOffsetForVarlen = context.EtagIgnoredOffset = Constants.EtagSize; + context.EtagIgnoredEnd = value.LengthWithoutMetadata; + context.ExistingEtag = *(long*)value.ToPointer(); } else { - EtagIgnoredOffset = 0; - EtagIgnoredEnd = -1; - ExistingEtag = Constants.BaseEtag; + context.EtagOffsetForVarlen = context.EtagIgnoredOffset = 0; + context.EtagIgnoredEnd = -1; + context.ExistingEtag = Constants.BaseEtag; } - - return this; } } } \ No newline at end of file diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 5dd891ba1d..be263311c3 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -293,7 +293,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; // Copy Inplace Update worker is the first in the potential pipeline of calling NeedCopyUpdate and CopyUpdater the following line will keep a precomputed values to use after this - input.etagOffsetManagementContext = input.etagOffsetManagementContext.CalculateOffsets(recordInfo.ETag, ref value); + EtagOffsetManagementContext.CalculateOffsets(ref input.etagOffsetManagementContext, recordInfo.ETag, ref value); int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index dca72a6a84..c24689d5b7 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -132,8 +132,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - int allocationForEtag = input.header.CheckWithEtagFlagMultiplier * Constants.EtagSize; - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize + allocationForEtag; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize + input.etagOffsetManagementContext.EtagOffsetForVarlen; } } @@ -145,7 +144,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) var cmd = input.header.cmd; // use the precomputed value - int etagOffset = input.etagOffsetManagementContext.EtagOffsetBasedOnInputHeaderOrRecordInfo; + int etagOffset = input.etagOffsetManagementContext.EtagOffsetForVarlen; switch (cmd) { diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index a8586c3e57..c8ae15096a 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -638,7 +638,10 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S input.arg1 = DateTimeOffset.UtcNow.Ticks + TimeSpan.FromMilliseconds(expireTimeMs).Ticks; if (withEtag) + { input.header.SetWithEtagFlag(); + EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); + } var setStatus = SET_Conditional(ref newKey, ref input, ref context); @@ -665,7 +668,10 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S input.parseState = parseState; if (withEtag) + { input.header.SetWithEtagFlag(); + EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); + } var setStatus = SET_Conditional(ref newKey, ref input, ref context); From ff459e9d9b9b3c08e953c8be6e94833d39e517a6 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sun, 29 Dec 2024 01:29:35 -0800 Subject: [PATCH 58/87] wippy --- libs/server/Resp/BasicCommands.cs | 6 +- libs/server/Storage/EtagOffsetManagement.cs | 6 +- .../Storage/Functions/MainStore/RMWMethods.cs | 56 ++++++++----------- 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ebfa8a9f11..3d74306ccf 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -787,7 +787,7 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) bool withEtag = etagOption == EtagOption.WITHETAG; - var isHighPrecision = expOption == ExpirationOption.PX; + bool isHighPrecision = expOption == ExpirationOption.PX; switch (expOption) { @@ -803,10 +803,10 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, - getValue, highPrecision: isHighPrecision, withEtag, ref storageApi); + getValue, isHighPrecision, withEtag, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, highPrecision: isHighPrecision, withEtag, ref storageApi); + getValue, isHighPrecision, withEtag, ref storageApi); } break; case ExpirationOption.KEEPTTL: diff --git a/libs/server/Storage/EtagOffsetManagement.cs b/libs/server/Storage/EtagOffsetManagement.cs index 75b4f853f7..57b51fe1d5 100644 --- a/libs/server/Storage/EtagOffsetManagement.cs +++ b/libs/server/Storage/EtagOffsetManagement.cs @@ -1,12 +1,9 @@ -using System.Runtime.CompilerServices; -using System.Threading; using Tsavorite.core; namespace Garnet.server { public struct EtagOffsetManagementContext { - // default values for when no Etag exists on a record public int EtagIgnoredOffset { get; private set; } public int EtagIgnoredEnd { get; private set; } @@ -30,7 +27,8 @@ public static unsafe void CalculateOffsets(ref EtagOffsetManagementContext conte } else { - context.EtagOffsetForVarlen = context.EtagIgnoredOffset = 0; + // default values for when no Etag exists on a record + context.EtagIgnoredOffset = 0; context.EtagIgnoredEnd = -1; context.ExistingEtag = Constants.BaseEtag; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index be263311c3..4d9fe4ce2d 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -81,14 +81,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: - bool withEtag = input.header.CheckWithEtagFlag(); - int spaceForEtag = 0; - if (withEtag) - { - spaceForEtag = Constants.EtagSize; - recordInfo.SetHasETag(); - } - + int spaceForEtag = input.etagOffsetManagementContext.EtagOffsetForVarlen; // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -97,8 +90,9 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB value.ShrinkSerializedLength(newInputValue.Length + metadataSize + spaceForEtag); value.ExtraMetadata = input.arg1; newInputValue.CopyTo(value.AsSpan(spaceForEtag)); - if (withEtag) + if (input.header.CheckWithEtagFlag()) { + recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag *(long*)value.ToPointer() = Constants.BaseEtag + 1; // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX @@ -107,21 +101,16 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB break; case RespCommand.SETKEEPTTL: - withEtag = input.header.CheckWithEtagFlag(); - spaceForEtag = 0; - if (withEtag) - { - spaceForEtag = Constants.EtagSize; - recordInfo.SetHasETag(); - } + spaceForEtag = input.etagOffsetManagementContext.EtagOffsetForVarlen; // Copy input to value, retain metadata in value var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; value.ShrinkSerializedLength(value.MetadataSize + setValue.Length + spaceForEtag); setValue.CopyTo(value.AsSpan(spaceForEtag)); - if (withEtag) + if (input.header.CheckWithEtagFlag()) { + recordInfo.SetHasETag(); *(long*)value.ToPointer() = Constants.BaseEtag + 1; // Copy initial etag to output CopyRespNumber(Constants.BaseEtag + 1, ref output); @@ -290,8 +279,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return false; } - RespCommand cmd = input.header.cmd; - // Copy Inplace Update worker is the first in the potential pipeline of calling NeedCopyUpdate and CopyUpdater the following line will keep a precomputed values to use after this EtagOffsetManagementContext.CalculateOffsets(ref input.etagOffsetManagementContext, recordInfo.ETag, ref value); @@ -299,6 +286,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; long oldEtag = input.etagOffsetManagementContext.ExistingEtag; + RespCommand cmd = input.header.cmd; switch (cmd) { case RespCommand.SETEXNX: @@ -308,10 +296,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - - // when called withetag all output needs to be placed on the buffer - if (input.header.CheckWithEtagFlag()) + else if (input.header.CheckWithEtagFlag()) { + // when called withetag all output needs to be placed on the buffer // EXX when unsuccesful will write back NIL CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } @@ -399,9 +386,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - if (inputHeaderHasEtag) - recordInfo.SetHasETag(); - // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); @@ -414,6 +398,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (inputHeaderHasEtag) { + recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; // withetag flag means we need to write etag back to the output buffer CopyRespNumber(oldEtag + 1, ref output); @@ -429,7 +414,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // If withEtag is called we return the etag back to the user nextUpdateEtagOffset = etagIgnoredOffset; nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!input.header.CheckWithEtagFlag()) + inputHeaderHasEtag = input.header.CheckWithEtagFlag(); + if (!inputHeaderHasEtag) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record nextUpdateEtagOffset = 0; @@ -456,9 +442,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - if (input.header.CheckWithEtagFlag()) - recordInfo.SetHasETag(); - // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.ShrinkSerializedLength(setValue.Length + value.MetadataSize + etagIgnoredOffset); @@ -467,8 +450,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (input.header.CheckWithEtagFlag()) + if (inputHeaderHasEtag) { + recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; // withetag flag means we need to write etag back to the output buffer CopyRespNumber(oldEtag + 1, ref output); @@ -759,13 +743,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { // offsets are precomputed at inplace updater - int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; - long existingEtag = input.etagOffsetManagementContext.ExistingEtag; switch (input.header.cmd) { case RespCommand.SETIFMATCH: + int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; + long existingEtag = input.etagOffsetManagementContext.ExistingEtag; long etagToCheckWith = input.parseState.GetLong(1); if (existingEtag != etagToCheckWith) @@ -785,6 +769,10 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB return false; } + + etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; + etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; + // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { @@ -815,7 +803,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(input.etagOffsetManagementContext.EtagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; From 110368f821dcc6488249342a96d93b33ae984f97 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sun, 29 Dec 2024 21:42:05 -0800 Subject: [PATCH 59/87] Try adding switch case --- libs/server/Resp/BasicCommands.cs | 21 ++++++++++++++++--- .../Functions/MainStore/PrivateMethods.cs | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 3d74306ccf..ec748b66ea 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -793,20 +793,35 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { case ExpirationOption.None: case ExpirationOption.EX: + switch (existOptions) + { + case ExistOptions.None: + return getValue || withEtag + ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, + false, withEtag, ref storageApi) + : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update + case ExistOptions.XX: + return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, + getValue, false, withEtag, ref storageApi); + case ExistOptions.NX: + return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, + getValue, false, withEtag, ref storageApi); + } + break; case ExpirationOption.PX: switch (existOptions) { case ExistOptions.None: return getValue || withEtag ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, - isHighPrecision, withEtag, ref storageApi) + true, withEtag, ref storageApi) : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, - getValue, isHighPrecision, withEtag, ref storageApi); + getValue, true, withEtag, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, isHighPrecision, withEtag, ref storageApi); + getValue, true, withEtag, ref storageApi); } break; case ExpirationOption.KEEPTTL: diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 4e50098c10..21512e17a4 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -460,6 +460,7 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, long n internal static bool CheckExpiry(ref SpanByte src) => src.ExtraMetadata < DateTimeOffset.UtcNow.Ticks; + // HK TODO: SUSPECT INCY IS JUST GETTING SKIPPED static bool InPlaceUpdateNumber(long val, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, int valueOffset) { var fNeg = false; From 04fab8c7da2f812c7d460cd97c380d262ac4660c Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sun, 29 Dec 2024 22:02:09 -0800 Subject: [PATCH 60/87] remove redundant metadatasize calc --- .../server/Storage/Functions/MainStore/VarLenInputMethods.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index c24689d5b7..e6357dd2e0 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -118,12 +118,11 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + ndigits; default: - var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); if (cmd > RespCommandExtensions.LastValidCommand) { var functions = functionsState.GetCustomCommandFunctions((ushort)cmd); // Compute metadata size for result - metadataSize = input.arg1 switch + int metadataSize = input.arg1 switch { -1 => 0, 0 => 0, @@ -132,7 +131,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + metadataSize + input.etagOffsetManagementContext.EtagOffsetForVarlen; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + input.arg1 == 0 ? 0 : sizeof(long) + input.etagOffsetManagementContext.EtagOffsetForVarlen; } } From 24505c698a875fcf59cc77f03ed640cc72c7fb90 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Sun, 29 Dec 2024 22:15:55 -0800 Subject: [PATCH 61/87] add flag for set to parser --- libs/server/Resp/Parser/RespCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 085a9d3dd0..9563ed0cbb 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -696,7 +696,7 @@ private RespCommand FastParseCommand(out int count) // Commands with dynamic number of arguments >= ((6 << 4) | 2) and <= ((6 << 4) | 3) when lastWord == MemoryMarshal.Read("RENAME\r\n"u8) => RespCommand.RENAME, >= ((8 << 4) | 2) and <= ((8 << 4) | 3) when lastWord == MemoryMarshal.Read("NAMENX\r\n"u8) && *(ushort*)(ptr + 8) == MemoryMarshal.Read("RE"u8) => RespCommand.RENAMENX, - >= ((3 << 4) | 3) and <= ((3 << 4) | 6) when lastWord == MemoryMarshal.Read("3\r\nSET\r\n"u8) => RespCommand.SETEXNX, + >= ((3 << 4) | 3) and <= ((3 << 4) | 7) when lastWord == MemoryMarshal.Read("3\r\nSET\r\n"u8) => RespCommand.SETEXNX, >= ((5 << 4) | 1) and <= ((5 << 4) | 3) when lastWord == MemoryMarshal.Read("\nGETEX\r\n"u8) => RespCommand.GETEX, >= ((6 << 4) | 0) and <= ((6 << 4) | 9) when lastWord == MemoryMarshal.Read("RUNTXP\r\n"u8) => RespCommand.RUNTXP, >= ((6 << 4) | 2) and <= ((6 << 4) | 3) when lastWord == MemoryMarshal.Read("EXPIRE\r\n"u8) => RespCommand.EXPIRE, From fa5941b8773918c781f234fc652d2a8da59006bc Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 31 Dec 2024 16:16:49 -0800 Subject: [PATCH 62/87] fix bugs and add comments --- libs/common/RespReadUtils.cs | 42 ------------------- libs/server/InputHeader.cs | 18 ++++---- libs/server/Resp/BasicCommands.cs | 38 +++++------------ .../Storage/Functions/MainStore/RMWMethods.cs | 2 +- .../Functions/MainStore/VarLenInputMethods.cs | 2 +- test/Garnet.test/Resp/RespReadUtilsTests.cs | 23 ---------- 6 files changed, 23 insertions(+), 102 deletions(-) diff --git a/libs/common/RespReadUtils.cs b/libs/common/RespReadUtils.cs index 6f324b1542..a82ed7ee87 100644 --- a/libs/common/RespReadUtils.cs +++ b/libs/common/RespReadUtils.cs @@ -3,7 +3,6 @@ using System; using System.Buffers.Text; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -459,47 +458,6 @@ public static bool TryRead64Int(out long number, ref byte* ptr, byte* end, out b return true; } - /// - /// Given a buffer check if the value is nil ($-1\r\n) - /// If the value is nil it advances the buffer forward - /// - /// The starting position in the RESP string. Will be advanced if parsing is successful. - /// The current end of the RESP string. - /// - /// True if value is nil on the buffer, false if the value on buffer is not nil - public static bool ReadNil(ref byte* ptr, byte* end, out byte? unexpectedToken) - { - unexpectedToken = null; - if (end - ptr < 5) - { - return false; - } - - ReadOnlySpan expectedNilRepr = "$-1\r\n"u8; - - if (*(uint*)ptr != MemoryMarshal.Read(expectedNilRepr.Slice(0, 4)) || *(ptr + 4) != expectedNilRepr[4]) - { - ReadOnlySpan ptrNext5Bytes = new ReadOnlySpan(ptr, 5); - for (int i = 0; i < 5; i++) - { - // first place where the sequence differs we have found the unexpected token - if (expectedNilRepr[i] != ptrNext5Bytes[i]) - { - // move the pointer to the unexpected token - ptr += i; - unexpectedToken = ptrNext5Bytes[i]; - return false; - } - } - // If the sequence is not equal we shouldn't even reach this because atleast one byte should have mismatched - Debug.Assert(false); - return false; - } - - ptr += 5; - return true; - } - /// /// Tries to read a RESP array length header from the given ASCII-encoded RESP string /// and, if successful, moves the given ptr to the end of the length header. diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index 400b6ea191..107787c755 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -11,15 +11,16 @@ namespace Garnet.server { /// /// Flags used by append-only file (AOF/WAL) + /// The byte representation only use the last 3 bits of the byte since the lower 5 bits of the field used to store the flag stores other data in the case of Object types. + /// In the case of a Rawstring, the last 4 bits are used for flags, and the other 4 bits are unused of the byte. /// [Flags] public enum RespInputFlags : byte { /// - /// Flag indicating if a SET operation should either add an etag or respect the etag semantics for a value with an etag already - /// This is used for conditional setting. + /// Flag indicating an operation intending to add an etag for a RAWSTRING command /// - WithEtag = 1, + WithEtag = 16, /// /// Flag indicating a SET operation that returns the previous value @@ -45,6 +46,9 @@ public struct RespInputHeader /// Size of header /// public const int Size = 3; + + // Since we know WithEtag is not used with any Object types, we keep the flag mask to work with the last 3 bits as flags, + // and the other 5 bits for storing object associated flags. However, in the case of Rawstring we use the last 4 bits for flags, and let the others remain unused. internal const byte FlagMask = (byte)RespInputFlags.SetGet - 1; [FieldOffset(0)] @@ -130,17 +134,15 @@ internal ListOperation ListOp internal unsafe void SetSetGetFlag() => flags |= RespInputFlags.SetGet; /// - /// Set "WithEtag" flag, used to update the old etag of a key after conditionally setting it + /// Set "WithEtag" flag for the input header /// - internal unsafe void SetWithEtagFlag() => flags |= RespInputFlags.WithEtag; + internal void SetWithEtagFlag() => flags |= RespInputFlags.WithEtag; /// /// Check if the WithEtag flag is set /// /// - internal unsafe bool CheckWithEtagFlag() => (flags & RespInputFlags.WithEtag) != 0; - - internal int CheckWithEtagFlagMultiplier => (int)(flags & RespInputFlags.WithEtag); + internal bool CheckWithEtagFlag() => (flags & RespInputFlags.WithEtag) != 0; /// /// Check if record is expired, either deterministically during log replay, diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index ec748b66ea..cd1632233e 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -793,35 +793,20 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) { case ExpirationOption.None: case ExpirationOption.EX: - switch (existOptions) - { - case ExistOptions.None: - return getValue || withEtag - ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, - false, withEtag, ref storageApi) - : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update - case ExistOptions.XX: - return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, - getValue, false, withEtag, ref storageApi); - case ExistOptions.NX: - return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, false, withEtag, ref storageApi); - } - break; case ExpirationOption.PX: switch (existOptions) { case ExistOptions.None: return getValue || withEtag ? NetworkSET_Conditional(RespCommand.SET, expiry, ref sbKey, getValue, - true, withEtag, ref storageApi) + isHighPrecision, withEtag, ref storageApi) : NetworkSET_EX(RespCommand.SET, expOption, expiry, ref sbKey, ref sbVal, ref storageApi); // Can perform a blind update case ExistOptions.XX: return NetworkSET_Conditional(RespCommand.SETEXXX, expiry, ref sbKey, - getValue, true, withEtag, ref storageApi); + getValue, isHighPrecision, withEtag, ref storageApi); case ExistOptions.NX: return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, - getValue, true, withEtag, ref storageApi); + getValue, isHighPrecision, withEtag, ref storageApi); } break; case ExpirationOption.KEEPTTL: @@ -839,7 +824,6 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) return NetworkSET_Conditional(RespCommand.SETEXNX, expiry, ref sbKey, getValue, highPrecision: false, withEtag, ref storageApi); } - break; } @@ -881,17 +865,17 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref var input = new RawStringInput(cmd, ref parseState, startIdx: 1, arg1: inputArg); - if (withEtag) + if (getValue || withEtag) { - input.header.SetWithEtagFlag(); - EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); - } + if (withEtag) + { + input.header.SetWithEtagFlag(); + EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); + } - if (getValue) - input.header.SetSetGetFlag(); + if (getValue) + input.header.SetSetGetFlag(); - if (getValue || withEtag) - { // anything with getValue or withEtag always writes to the buffer in the happy path SpanByteAndMemory outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); GarnetStatus status = storageApi.SET_Conditional(ref key, diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 4d9fe4ce2d..f9c3622632 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -712,7 +712,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); - var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), ref input, value.AsSpan(etagIgnoredOffset), ref valueLength, ref outp, ref rmwInfo); + var ret = functions.InPlaceUpdater(key.AsReadOnlySpan(), ref input, value.AsSpan(), ref valueLength, ref outp, ref rmwInfo); Debug.Assert(valueLength <= value.LengthWithoutMetadata); // Adjust value length if user shrinks it diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index e6357dd2e0..4498f135d6 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -131,7 +131,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + input.arg1 == 0 ? 0 : sizeof(long) + input.etagOffsetManagementContext.EtagOffsetForVarlen; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + input.etagOffsetManagementContext.EtagOffsetForVarlen; } } diff --git a/test/Garnet.test/Resp/RespReadUtilsTests.cs b/test/Garnet.test/Resp/RespReadUtilsTests.cs index c0a576d7c3..ad0e0c66ff 100644 --- a/test/Garnet.test/Resp/RespReadUtilsTests.cs +++ b/test/Garnet.test/Resp/RespReadUtilsTests.cs @@ -289,28 +289,5 @@ public static unsafe void ReadBoolWithLengthHeaderTest(string text, bool expecte ClassicAssert.IsTrue(start == end); } } - - /// - /// Tests that Readnil successfully parses valid inputs. - /// - [TestCase("", false, null)] // Too short - [TestCase("S$-1\r\n", false, "S")] // Long enough but not nil leading - [TestCase("$-1\n1738\r\n", false, "\n")] // Long enough but not nil - [TestCase("$-1\r\n", true, null)] // exact nil - [TestCase("$-1\r\nxyzextra", true, null)] // leading nil but with extra bytes after - public static unsafe void ReadNilTest(string testSequence, bool expected, string firstMismatch) - { - ReadOnlySpan testSeq = new ReadOnlySpan(Encoding.ASCII.GetBytes(testSequence)); - - fixed (byte* ptr = testSeq) - { - byte* start = ptr; - byte* end = ptr + testSeq.Length; - var isNil = RespReadUtils.ReadNil(ref start, end, out byte? unexpectedToken); - - ClassicAssert.AreEqual(expected, isNil); - ClassicAssert.AreEqual((byte?)firstMismatch?[0], unexpectedToken); - } - } } } \ No newline at end of file From dab924c8d7e75ab33260a6b3a5775626887271c8 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 6 Jan 2025 09:58:23 -0800 Subject: [PATCH 63/87] Fix ACL and Txn unit test --- libs/resources/RespCommandsInfo.json | 69 ++++++++++++------- libs/server/ACL/ACLParser.cs | 2 +- libs/server/ACL/CommandPermissionSet.cs | 4 +- libs/server/InputHeader.cs | 6 +- libs/server/Resp/Parser/RespCommand.cs | 6 +- .../Resp/RespServerSessionSlotVerify.cs | 2 +- libs/server/Storage/EtagOffsetManagement.cs | 4 ++ .../Storage/Functions/MainStore/RMWMethods.cs | 2 +- libs/server/Transaction/TxnRespCommands.cs | 2 + test/Garnet.test/Resp/ACL/RespCommandTests.cs | 2 +- test/Garnet.test/Resp/RespReadUtilsTests.cs | 1 - 11 files changed, 65 insertions(+), 35 deletions(-) diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index 72f103e49b..33142bd5d3 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -1506,9 +1506,21 @@ "LastKey": 1, "Step": 1, "AclCategories": "Fast, String, Read", - "Tips": null, - "KeySpecifications": null, - "SubCommands": null + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + } + ] }, { "Command": "GETRANGE", @@ -1570,9 +1582,21 @@ "LastKey": 1, "Step": 1, "AclCategories": "Fast, String, Read", - "Tips": null, - "KeySpecifications": null, - "SubCommands": null + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + } + ] }, { "Command": "HDEL", @@ -3503,19 +3527,6 @@ } ] }, - { - "Command": "SETEXNX", - "Name": "SETEXNX", - "Arity": -3, - "Flags": "NONE", - "FirstKey": 1, - "LastKey": 1, - "Step": 1, - "AclCategories": "Fast, String, Write, Transaction", - "Tips": null, - "KeySpecifications": null, - "SubCommands": null - }, { "Command": "SETIFMATCH", "Name": "SETIFMATCH", @@ -3525,10 +3536,22 @@ "FirstKey": 1, "LastKey": 1, "Step": 1, - "AclCategories": "Fast, String, Write", - "Tips": null, - "KeySpecifications": null, - "SubCommands": null + "AclCategories": "Slow, String, Write", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RW, Access, Update, VariableFlags" + } + ] }, { "Command": "SETRANGE", diff --git a/libs/server/ACL/ACLParser.cs b/libs/server/ACL/ACLParser.cs index f033f3042a..fcd6183c8c 100644 --- a/libs/server/ACL/ACLParser.cs +++ b/libs/server/ACL/ACLParser.cs @@ -296,7 +296,7 @@ static bool IsValidParse(RespCommand command, ReadOnlySpan fromStr) // Some commands aren't really commands, so ACLs shouldn't accept their names static bool IsInvalidCommandToAcl(RespCommand command) - => command == RespCommand.INVALID || command == RespCommand.NONE || command.NormalizeForACLs() != command; + => command == RespCommand.INVALID || command == RespCommand.NONE || command.Normalize() != command; } /// diff --git a/libs/server/ACL/CommandPermissionSet.cs b/libs/server/ACL/CommandPermissionSet.cs index fc9f0cb694..780db48c48 100644 --- a/libs/server/ACL/CommandPermissionSet.cs +++ b/libs/server/ACL/CommandPermissionSet.cs @@ -91,7 +91,7 @@ public CommandPermissionSet Copy() /// public void AddCommand(RespCommand command) { - Debug.Assert(command.NormalizeForACLs() == command, "Cannot control access to this command, it's an implementation detail"); + Debug.Assert(command.Normalize() == command, "Cannot control access to this command, it's an implementation detail"); int index = (int)command; int ulongIndex = index / 64; @@ -118,7 +118,7 @@ public void AddCommand(RespCommand command) /// public void RemoveCommand(RespCommand command) { - Debug.Assert(command.NormalizeForACLs() == command, "Cannot control access to this command, it's an implementation detail"); + Debug.Assert(command.Normalize() == command, "Cannot control access to this command, it's an implementation detail"); // Can't remove access to these commands if (command.IsNoAuth()) diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index 107787c755..83fe9f284d 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -13,12 +13,13 @@ namespace Garnet.server /// Flags used by append-only file (AOF/WAL) /// The byte representation only use the last 3 bits of the byte since the lower 5 bits of the field used to store the flag stores other data in the case of Object types. /// In the case of a Rawstring, the last 4 bits are used for flags, and the other 4 bits are unused of the byte. + /// NOTE: This will soon be expanded as a part of a breaking change to make WithEtag bit compatible with object store as well. /// [Flags] public enum RespInputFlags : byte { /// - /// Flag indicating an operation intending to add an etag for a RAWSTRING command + /// Flag indicating an operation intending to add an etag for a RAWSTRING command. /// WithEtag = 16, @@ -323,7 +324,8 @@ public struct RawStringInput : IStoreInput { /// /// Mutable state we keep around for efficient EtagOffsetManagement, this will be removed when ETag is stored at the record level separately and does not require offset management. - /// NOTE: We do not serialize this to disk or read it from disk, it is only used in memory, and a temporary solution to keep track of Etag offsets across method calls. + /// NOTE: We do not serialize this to disk or read it from disk, it is only kept in volatile memory. The WITHETAG flag that may or may not be stored in the header is used to conditionally + /// initialize the values for this field. /// public EtagOffsetManagementContext etagOffsetManagementContext; diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 9563ed0cbb..6ca9f8e9f7 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -434,10 +434,10 @@ public static bool IsAofIndependent(this RespCommand cmd) /// /// Turns any not-quite-a-real-command entries in into the equivalent command - /// for ACL'ing purposes. + /// for ACL'ing purposes and reading command info purposes /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RespCommand NormalizeForACLs(this RespCommand cmd) + public static RespCommand Normalize(this RespCommand cmd) { return cmd switch @@ -452,7 +452,7 @@ public static RespCommand NormalizeForACLs(this RespCommand cmd) } /// - /// Reverses , producing all the equivalent s which are covered by . + /// Reverses , producing all the equivalent s which are covered by . /// public static ReadOnlySpan ExpandForACLs(this RespCommand cmd) { diff --git a/libs/server/Resp/RespServerSessionSlotVerify.cs b/libs/server/Resp/RespServerSessionSlotVerify.cs index 323fb50be0..2e7c450c79 100644 --- a/libs/server/Resp/RespServerSessionSlotVerify.cs +++ b/libs/server/Resp/RespServerSessionSlotVerify.cs @@ -30,7 +30,7 @@ bool CanServeSlot(RespCommand cmd) if (!cmd.IsDataCommand()) return true; - cmd = cmd.NormalizeForACLs(); + cmd = cmd.Normalize(); if (!RespCommandsInfo.TryFastGetRespCommandInfo(cmd, out var commandInfo)) // This only happens if we failed to parse the json file return false; diff --git a/libs/server/Storage/EtagOffsetManagement.cs b/libs/server/Storage/EtagOffsetManagement.cs index 57b51fe1d5..f900335689 100644 --- a/libs/server/Storage/EtagOffsetManagement.cs +++ b/libs/server/Storage/EtagOffsetManagement.cs @@ -2,6 +2,10 @@ namespace Garnet.server { + /// + /// Offset acounting done to prevent the need for recalculation at different methods. This is passed as context along with RawStringInput. + /// Making it a struct makes sure the values are embedded as a part of RawStringInput. + /// public struct EtagOffsetManagementContext { public int EtagIgnoredOffset { get; private set; } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index f9c3622632..e32d314aa5 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -19,7 +19,6 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp { switch (input.header.cmd) { - case RespCommand.SETIFMATCH: case RespCommand.SETKEEPTTLXX: case RespCommand.PERSIST: case RespCommand.EXPIRE: @@ -29,6 +28,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.GETDEL: case RespCommand.GETEX: return false; + case RespCommand.SETIFMATCH: case RespCommand.SETEXXX: // when called withetag all output needs to be placed on the buffer if (input.header.CheckWithEtagFlag()) diff --git a/libs/server/Transaction/TxnRespCommands.cs b/libs/server/Transaction/TxnRespCommands.cs index e18d97a7c7..0f5c90b170 100644 --- a/libs/server/Transaction/TxnRespCommands.cs +++ b/libs/server/Transaction/TxnRespCommands.cs @@ -98,6 +98,8 @@ private bool NetworkEXEC() private bool NetworkSKIP(RespCommand cmd) { // Retrieve the meta-data for the command to do basic sanity checking for command arguments + // Normalize will turn internal "not-real commands" such as SETEXNX, and SETEXXX to the command info parent + cmd = cmd.Normalize(); if (!RespCommandsInfo.TryGetRespCommandInfo(cmd, out var commandInfo, txnOnly: true, logger)) { while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 60d6296c19..756c7191ea 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -98,7 +98,7 @@ public void AllCommandsCovered() // Check tests against RespCommand { - IEnumerable allValues = Enum.GetValues().Select(static x => x.NormalizeForACLs()).Distinct(); + IEnumerable allValues = Enum.GetValues().Select(static x => x.Normalize()).Distinct(); IEnumerable testableValues = allValues .Except([RespCommand.NONE, RespCommand.INVALID]) diff --git a/test/Garnet.test/Resp/RespReadUtilsTests.cs b/test/Garnet.test/Resp/RespReadUtilsTests.cs index ad0e0c66ff..7860dd7bc2 100644 --- a/test/Garnet.test/Resp/RespReadUtilsTests.cs +++ b/test/Garnet.test/Resp/RespReadUtilsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System; using System.Text; using Garnet.common; using Garnet.common.Parsing; From 52002980034c92f5d96d5c91fab2b86eac9054f8 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 6 Jan 2025 10:06:19 -0800 Subject: [PATCH 64/87] remove leftover --- libs/server/API/GarnetStatus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server/API/GarnetStatus.cs b/libs/server/API/GarnetStatus.cs index 0972da6fb6..2277461ad4 100644 --- a/libs/server/API/GarnetStatus.cs +++ b/libs/server/API/GarnetStatus.cs @@ -23,6 +23,6 @@ public enum GarnetStatus : byte /// /// Wrong type /// - WRONGTYPE, + WRONGTYPE } } \ No newline at end of file From 5671d3dd2823df1d5541c4093e0cd420d9949ec7 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 6 Jan 2025 13:03:32 -0800 Subject: [PATCH 65/87] reduce extra comparison --- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index e32d314aa5..748da00abf 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -826,7 +826,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); RespCommand cmd = input.header.cmd; - bool shouldUpdateEtag = true; + bool shouldUpdateEtag = recordInfo.ETag; // offsets are precomputed at InPlaceUpdater int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; @@ -1170,7 +1170,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - if (recordInfo.ETag && shouldUpdateEtag) + if (shouldUpdateEtag) { *(long*)newValue.ToPointer() = oldEtag + 1; } From 38c266ff5714e8c2e95d965542a9f3418e575949 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 6 Jan 2025 16:27:59 -0800 Subject: [PATCH 66/87] leave input header alone --- libs/server/AOF/AofProcessor.cs | 3 - libs/server/Constants.cs | 2 +- libs/server/InputHeader.cs | 7 -- libs/server/Resp/BasicCommands.cs | 3 - libs/server/Storage/EtagOffsetManagement.cs | 41 ---------- .../Storage/Functions/EtagOffsetManagement.cs | 38 ++++++++++ .../Storage/Functions/FunctionsState.cs | 2 + .../Storage/Functions/MainStore/RMWMethods.cs | 74 ++++++++++++------- .../Functions/MainStore/VarLenInputMethods.cs | 4 +- .../Storage/Session/MainStore/MainStoreOps.cs | 2 - 10 files changed, 89 insertions(+), 87 deletions(-) delete mode 100644 libs/server/Storage/EtagOffsetManagement.cs create mode 100644 libs/server/Storage/Functions/EtagOffsetManagement.cs diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index acf152b6b8..94bf35913c 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -320,9 +320,6 @@ static unsafe void StoreRMW(BasicContext public struct RawStringInput : IStoreInput { - /// - /// Mutable state we keep around for efficient EtagOffsetManagement, this will be removed when ETag is stored at the record level separately and does not require offset management. - /// NOTE: We do not serialize this to disk or read it from disk, it is only kept in volatile memory. The WITHETAG flag that may or may not be stored in the header is used to conditionally - /// initialize the values for this field. - /// - public EtagOffsetManagementContext etagOffsetManagementContext; - /// /// Common input header for Garnet /// diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index cd1632233e..f24fdaa1e6 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -868,10 +868,7 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref if (getValue || withEtag) { if (withEtag) - { input.header.SetWithEtagFlag(); - EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); - } if (getValue) input.header.SetSetGetFlag(); diff --git a/libs/server/Storage/EtagOffsetManagement.cs b/libs/server/Storage/EtagOffsetManagement.cs deleted file mode 100644 index f900335689..0000000000 --- a/libs/server/Storage/EtagOffsetManagement.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Tsavorite.core; - -namespace Garnet.server -{ - /// - /// Offset acounting done to prevent the need for recalculation at different methods. This is passed as context along with RawStringInput. - /// Making it a struct makes sure the values are embedded as a part of RawStringInput. - /// - public struct EtagOffsetManagementContext - { - public int EtagIgnoredOffset { get; private set; } - - public int EtagIgnoredEnd { get; private set; } - - public long ExistingEtag { get; private set; } - - public int EtagOffsetForVarlen { get; private set; } - - public static void SetEtagOffsetBasedOnInputHeader(ref EtagOffsetManagementContext context) - { - context.EtagOffsetForVarlen = Constants.EtagSize; - } - - public static unsafe void CalculateOffsets(ref EtagOffsetManagementContext context, bool hasEtag, ref SpanByte value) - { - if (hasEtag) - { - context.EtagOffsetForVarlen = context.EtagIgnoredOffset = Constants.EtagSize; - context.EtagIgnoredEnd = value.LengthWithoutMetadata; - context.ExistingEtag = *(long*)value.ToPointer(); - } - else - { - // default values for when no Etag exists on a record - context.EtagIgnoredOffset = 0; - context.EtagIgnoredEnd = -1; - context.ExistingEtag = Constants.BaseEtag; - } - } - } -} \ No newline at end of file diff --git a/libs/server/Storage/Functions/EtagOffsetManagement.cs b/libs/server/Storage/Functions/EtagOffsetManagement.cs new file mode 100644 index 0000000000..009838a264 --- /dev/null +++ b/libs/server/Storage/Functions/EtagOffsetManagement.cs @@ -0,0 +1,38 @@ +using Tsavorite.core; + +namespace Garnet.server +{ + /// + /// Offset acounting done to prevent the need for recalculation at different methods. + /// + internal sealed class EtagOffsetManagementContext + { + public byte EtagIgnoredOffset { get; private set; } + + public byte EtagOffsetForVarlen { get; private set; } + + public int EtagIgnoredEnd { get; private set; } + + public long ExistingEtag { get; private set; } + + public void SetEtagOffsetBasedOnInputHeader(bool withEtag) + { + this.EtagOffsetForVarlen = !withEtag ? (byte)0 : Constants.EtagSize; + } + + public unsafe void CalculateOffsets(bool hasEtag, ref SpanByte value) + { + if (!hasEtag) + { + this.EtagIgnoredOffset = 0; + this.EtagIgnoredEnd = -1; + this.ExistingEtag = Constants.BaseEtag; + return; + } + + this.EtagOffsetForVarlen = this.EtagIgnoredOffset = Constants.EtagSize; + this.EtagIgnoredEnd = value.LengthWithoutMetadata; + this.ExistingEtag = *(long*)value.ToPointer(); + } + } +} \ No newline at end of file diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index 055ad9f675..7ef9a504c9 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -19,6 +19,7 @@ internal sealed class FunctionsState public readonly CacheSizeTracker objectStoreSizeTracker; public readonly GarnetObjectSerializer garnetObjectSerializer; public bool StoredProcMode; + public readonly EtagOffsetManagementContext etagOffsetManagementContext; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, MemoryPool memoryPool, CacheSizeTracker objectStoreSizeTracker, GarnetObjectSerializer garnetObjectSerializer) @@ -29,6 +30,7 @@ public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionM this.memoryPool = memoryPool ?? MemoryPool.Shared; this.objectStoreSizeTracker = objectStoreSizeTracker; this.garnetObjectSerializer = garnetObjectSerializer; + this.etagOffsetManagementContext = new EtagOffsetManagementContext(); } public CustomRawStringFunctions GetCustomCommandFunctions(int id) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 748da00abf..17a3de6579 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -47,6 +47,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp output.Length = outp.Length; return ret; } + this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); return true; } } @@ -81,7 +82,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: - int spaceForEtag = input.etagOffsetManagementContext.EtagOffsetForVarlen; + int spaceForEtag = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -90,32 +91,40 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB value.ShrinkSerializedLength(newInputValue.Length + metadataSize + spaceForEtag); value.ExtraMetadata = input.arg1; newInputValue.CopyTo(value.AsSpan(spaceForEtag)); - if (input.header.CheckWithEtagFlag()) + + if (!input.header.CheckWithEtagFlag()) + break; + else { recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag *(long*)value.ToPointer() = Constants.BaseEtag + 1; // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX CopyRespNumber(Constants.BaseEtag + 1, ref output); + break; + } - break; case RespCommand.SETKEEPTTL: - spaceForEtag = input.etagOffsetManagementContext.EtagOffsetForVarlen; + spaceForEtag = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; // Copy input to value, retain metadata in value var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; value.ShrinkSerializedLength(value.MetadataSize + setValue.Length + spaceForEtag); setValue.CopyTo(value.AsSpan(spaceForEtag)); - if (input.header.CheckWithEtagFlag()) + if (!input.header.CheckWithEtagFlag()) + { + break; + } + else { recordInfo.SetHasETag(); *(long*)value.ToPointer() = Constants.BaseEtag + 1; // Copy initial etag to output CopyRespNumber(Constants.BaseEtag + 1, ref output); + break; } - break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETEXXX: @@ -280,11 +289,12 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // Copy Inplace Update worker is the first in the potential pipeline of calling NeedCopyUpdate and CopyUpdater the following line will keep a precomputed values to use after this - EtagOffsetManagementContext.CalculateOffsets(ref input.etagOffsetManagementContext, recordInfo.ETag, ref value); + this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); + this.functionsState.etagOffsetManagementContext.CalculateOffsets(recordInfo.ETag, ref value); - int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; - long oldEtag = input.etagOffsetManagementContext.ExistingEtag; + int etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; + long oldEtag = this.functionsState.etagOffsetManagementContext.ExistingEtag; RespCommand cmd = input.header.cmd; switch (cmd) @@ -356,6 +366,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; bool inputHeaderHasEtag = input.header.CheckWithEtagFlag(); + if (!inputHeaderHasEtag) { // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record @@ -396,17 +407,19 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(nextUpdateEtagOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (inputHeaderHasEtag) + if (!inputHeaderHasEtag) + { + break; + } + else { recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; // withetag flag means we need to write etag back to the output buffer CopyRespNumber(oldEtag + 1, ref output); - // early return since we already updated etag + // return since we already updated etag return true; } - - break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: // If the user calls withetag then we need to either update an existing etag and set the value @@ -450,7 +463,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (inputHeaderHasEtag) + if (!inputHeaderHasEtag) + { + break; + } + else { recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; @@ -460,7 +477,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; } - break; case RespCommand.PEXPIRE: case RespCommand.EXPIRE: @@ -747,18 +763,21 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB switch (input.header.cmd) { case RespCommand.SETIFMATCH: - int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; - long existingEtag = input.etagOffsetManagementContext.ExistingEtag; + int etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; + long existingEtag = this.functionsState.etagOffsetManagementContext.ExistingEtag; long etagToCheckWith = input.parseState.GetLong(1); - if (existingEtag != etagToCheckWith) + if (existingEtag == etagToCheckWith) + { + return true; + } + else { CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); return false; } - return true; case RespCommand.SETEXNX: // Expired data, return false immediately // ExpireAndResume ensures that we set as new value, since it does not exist @@ -769,9 +788,8 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB return false; } - - etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; - etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; + etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; + etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) @@ -803,7 +821,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(input.etagOffsetManagementContext.EtagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -829,9 +847,9 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte bool shouldUpdateEtag = recordInfo.ETag; // offsets are precomputed at InPlaceUpdater - int etagIgnoredOffset = input.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = input.etagOffsetManagementContext.EtagIgnoredEnd; - long oldEtag = input.etagOffsetManagementContext.ExistingEtag; + int etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; + int etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; + long oldEtag = this.functionsState.etagOffsetManagementContext.ExistingEtag; switch (cmd) { diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 4498f135d6..7aa665acb4 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -131,7 +131,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + input.etagOffsetManagementContext.EtagOffsetForVarlen; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; } } @@ -143,7 +143,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) var cmd = input.header.cmd; // use the precomputed value - int etagOffset = input.etagOffsetManagementContext.EtagOffsetForVarlen; + int etagOffset = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; switch (cmd) { diff --git a/libs/server/Storage/Session/MainStore/MainStoreOps.cs b/libs/server/Storage/Session/MainStore/MainStoreOps.cs index c8ae15096a..41c3bf67bb 100644 --- a/libs/server/Storage/Session/MainStore/MainStoreOps.cs +++ b/libs/server/Storage/Session/MainStore/MainStoreOps.cs @@ -640,7 +640,6 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S if (withEtag) { input.header.SetWithEtagFlag(); - EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); } var setStatus = SET_Conditional(ref newKey, ref input, ref context); @@ -670,7 +669,6 @@ private unsafe GarnetStatus RENAME(ArgSlice oldKeySlice, ArgSlice newKeySlice, S if (withEtag) { input.header.SetWithEtagFlag(); - EtagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(ref input.etagOffsetManagementContext); } var setStatus = SET_Conditional(ref newKey, ref input, ref context); From e67cf9afe2bd105baee46b739d6cd9d92906c616 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 6 Jan 2025 17:07:39 -0800 Subject: [PATCH 67/87] wip --- libs/server/InputHeader.cs | 5 ++ .../Storage/Functions/MainStore/RMWMethods.cs | 59 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/libs/server/InputHeader.cs b/libs/server/InputHeader.cs index 2ff888d47f..5abff66a89 100644 --- a/libs/server/InputHeader.cs +++ b/libs/server/InputHeader.cs @@ -145,6 +145,11 @@ internal ListOperation ListOp /// internal bool CheckWithEtagFlag() => (flags & RespInputFlags.WithEtag) != 0; + /// + /// Check that neither SetGet nor WithEtag flag is set + /// + internal bool NotSetGetNorCheckWithEtag() => (flags & (RespInputFlags.SetGet | RespInputFlags.WithEtag)) == 0; + /// /// Check if record is expired, either deterministically during log replay, /// or based on current time in normal operation. diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 17a3de6579..68087249c6 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -37,6 +37,11 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } return false; + case RespCommand.SET: + case RespCommand.SETEXNX: + case RespCommand.SETKEEPTTL: + this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); + return true; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) { @@ -47,7 +52,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp output.Length = outp.Length; return ret; } - this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); + return true; } } @@ -92,19 +97,17 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB value.ExtraMetadata = input.arg1; newInputValue.CopyTo(value.AsSpan(spaceForEtag)); - if (!input.header.CheckWithEtagFlag()) - break; - else + // HK TODO maybe cache this calc in etagOffsetManagementContext + if (input.header.CheckWithEtagFlag()) { recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag *(long*)value.ToPointer() = Constants.BaseEtag + 1; // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX CopyRespNumber(Constants.BaseEtag + 1, ref output); - break; - } + break; case RespCommand.SETKEEPTTL: spaceForEtag = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; @@ -113,19 +116,16 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB value.ShrinkSerializedLength(value.MetadataSize + setValue.Length + spaceForEtag); setValue.CopyTo(value.AsSpan(spaceForEtag)); - if (!input.header.CheckWithEtagFlag()) - { - break; - } - else + if (input.header.CheckWithEtagFlag()) { recordInfo.SetHasETag(); *(long*)value.ToPointer() = Constants.BaseEtag + 1; // Copy initial etag to output CopyRespNumber(Constants.BaseEtag + 1, ref output); - break; } + break; + case RespCommand.SETKEEPTTLXX: case RespCommand.SETEXXX: case RespCommand.EXPIRE: @@ -288,7 +288,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return false; } - // Copy Inplace Update worker is the first in the potential pipeline of calling NeedCopyUpdate and CopyUpdater the following line will keep a precomputed values to use after this this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); this.functionsState.etagOffsetManagementContext.CalculateOffsets(recordInfo.ETag, ref value); @@ -300,13 +299,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re switch (cmd) { case RespCommand.SETEXNX: + if (input.header.NotSetGetNorCheckWithEtag()) + return true; + // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); } - else if (input.header.CheckWithEtagFlag()) + else { // when called withetag all output needs to be placed on the buffer // EXX when unsuccesful will write back NIL @@ -407,11 +409,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(nextUpdateEtagOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (!inputHeaderHasEtag) - { - break; - } - else + if (inputHeaderHasEtag) { recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; @@ -420,6 +418,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // return since we already updated etag return true; } + + break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: // If the user calls withetag then we need to either update an existing etag and set the value @@ -463,11 +463,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(etagIgnoredOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - if (!inputHeaderHasEtag) - { - break; - } - else + if (inputHeaderHasEtag) { recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; @@ -477,6 +473,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; } + break; case RespCommand.PEXPIRE: case RespCommand.EXPIRE: @@ -788,10 +785,14 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB return false; } + // since this block is only hit when this an update, the NX is violated and so we can return early from it without setting the value + + if (input.header.NotSetGetNorCheckWithEtag()) + return false; + etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; - // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. @@ -805,7 +806,6 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } - // since this block is only hit when this an update, the NX is violated and so we can return early from it without setting the value return false; case RespCommand.SETEXXX: // Expired data, return false immediately so we do not set, since it does not exist @@ -854,8 +854,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte switch (cmd) { case RespCommand.SETIFMATCH: - shouldUpdateEtag = false; - // Copy input to value Span dest = newValue.AsSpan(Constants.EtagSize); ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -874,7 +872,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte } long newEtag = oldEtag + 1; - *(long*)newValue.ToPointer() = newEtag; recordInfo.SetHasETag(); @@ -884,9 +881,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, writeDirect: true); - // early return since we already updated the ETag - return true; - + break; case RespCommand.SET: case RespCommand.SETEXXX: var nextUpdateEtagOffset = etagIgnoredOffset; From 40d9f6d8d70471bcb5bd725c2cc80d0c45b498fc Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Mon, 6 Jan 2025 17:59:14 -0800 Subject: [PATCH 68/87] wip --- .../Storage/Functions/MainStore/RMWMethods.cs | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 68087249c6..db6e3b6022 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -798,9 +798,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB // Copy value to output for the GET part of the command. CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } - - // when called withetag all output needs to be placed on the buffer - if (input.header.CheckWithEtagFlag()) + else if (input.header.CheckWithEtagFlag()) { // EXX when unsuccesful will write back NIL CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); @@ -854,6 +852,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte switch (cmd) { case RespCommand.SETIFMATCH: + shouldUpdateEtag = true; // Copy input to value Span dest = newValue.AsSpan(Constants.EtagSize); ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -887,6 +886,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte var nextUpdateEtagOffset = etagIgnoredOffset; var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; bool inputWithEtag = input.header.CheckWithEtagFlag(); + + // Common case if (!inputWithEtag) { // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record @@ -900,6 +901,20 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte nextUpdateEtagOffset = Constants.EtagSize; nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; recordInfo.SetHasETag(); + shouldUpdateEtag = true; + // removes the need for branching in common path at the end + if (inputWithEtag) + { + shouldUpdateEtag = true; + // withetag flag means we need to write etag back to the output buffer + CopyRespNumber(oldEtag + 1, ref output); + } + } + else + { + shouldUpdateEtag = true; + // removes the need for branching in common path at the end + CopyRespNumber(oldEtag + 1, ref output); } // Check if SetGet flag is set @@ -920,21 +935,14 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte newValue.ExtraMetadata = input.arg1; newInputValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); - if (inputWithEtag) - { - shouldUpdateEtag = false; - *(long*)newValue.ToPointer() = oldEtag + 1; - // withetag flag means we need to write etag back to the output buffer - CopyRespNumber(oldEtag + 1, ref output); - } - break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: nextUpdateEtagOffset = etagIgnoredOffset; nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - if (!input.header.CheckWithEtagFlag()) + inputWithEtag = input.header.CheckWithEtagFlag(); + if (!inputWithEtag) { // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record nextUpdateEtagOffset = 0; @@ -946,6 +954,17 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte nextUpdateEtagOffset = Constants.EtagSize; nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; recordInfo.SetHasETag(); + if (inputWithEtag) + { + shouldUpdateEtag = true; + CopyRespNumber(oldEtag + 1, ref output); + } + } + else + { + shouldUpdateEtag = true; + // withetag flag means we need to write etag back to the output buffer + CopyRespNumber(oldEtag + 1, ref output); } var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -963,13 +982,6 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Copy input to value, retain metadata of oldValue newValue.ExtraMetadata = oldValue.ExtraMetadata; setValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); - if (input.header.CheckWithEtagFlag()) - { - shouldUpdateEtag = false; - *(long*)newValue.ToPointer() = oldEtag + 1; - // withetag flag means we need to write etag back to the output buffer - CopyRespNumber(oldEtag + 1, ref output); - } break; From c5ed4a3695d4e9c7ded8eefac3abddb6274b826a Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 7 Jan 2025 15:09:27 -0800 Subject: [PATCH 69/87] reduce common path logic --- .github/workflows/ci-bdnbenchmark.yml | 3 +- .../Storage/Functions/EtagOffsetManagement.cs | 38 --- .../Storage/Functions/FunctionsState.cs | 3 +- .../Storage/Functions/MainStore/RMWMethods.cs | 248 +++++++++++------- .../Functions/MainStore/VarLenInputMethods.cs | 6 +- 5 files changed, 153 insertions(+), 145 deletions(-) delete mode 100644 libs/server/Storage/Functions/EtagOffsetManagement.cs diff --git a/.github/workflows/ci-bdnbenchmark.yml b/.github/workflows/ci-bdnbenchmark.yml index 7ca620bc73..271205673e 100644 --- a/.github/workflows/ci-bdnbenchmark.yml +++ b/.github/workflows/ci-bdnbenchmark.yml @@ -42,7 +42,8 @@ jobs: os: [ ubuntu-latest, windows-latest ] framework: [ 'net8.0' ] configuration: [ 'Release' ] - test: [ 'Operations.BasicOperations', 'Operations.ObjectOperations', 'Operations.HashObjectOperations', 'Cluster.ClusterMigrate', 'Cluster.ClusterOperations', 'Lua.LuaScripts', 'Operations.CustomOperations', 'Operations.RawStringOperations', 'Operations.ScriptOperations','Network.BasicOperations', 'Network.RawStringOperations' ] + # temp changes to make my testing loop faster + test: [ 'Operations.RawStringOperations', 'Network.RawStringOperations' ] steps: - name: Check out code uses: actions/checkout@v4 diff --git a/libs/server/Storage/Functions/EtagOffsetManagement.cs b/libs/server/Storage/Functions/EtagOffsetManagement.cs deleted file mode 100644 index 009838a264..0000000000 --- a/libs/server/Storage/Functions/EtagOffsetManagement.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Tsavorite.core; - -namespace Garnet.server -{ - /// - /// Offset acounting done to prevent the need for recalculation at different methods. - /// - internal sealed class EtagOffsetManagementContext - { - public byte EtagIgnoredOffset { get; private set; } - - public byte EtagOffsetForVarlen { get; private set; } - - public int EtagIgnoredEnd { get; private set; } - - public long ExistingEtag { get; private set; } - - public void SetEtagOffsetBasedOnInputHeader(bool withEtag) - { - this.EtagOffsetForVarlen = !withEtag ? (byte)0 : Constants.EtagSize; - } - - public unsafe void CalculateOffsets(bool hasEtag, ref SpanByte value) - { - if (!hasEtag) - { - this.EtagIgnoredOffset = 0; - this.EtagIgnoredEnd = -1; - this.ExistingEtag = Constants.BaseEtag; - return; - } - - this.EtagOffsetForVarlen = this.EtagIgnoredOffset = Constants.EtagSize; - this.EtagIgnoredEnd = value.LengthWithoutMetadata; - this.ExistingEtag = *(long*)value.ToPointer(); - } - } -} \ No newline at end of file diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index 7ef9a504c9..3f75850468 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -19,7 +19,7 @@ internal sealed class FunctionsState public readonly CacheSizeTracker objectStoreSizeTracker; public readonly GarnetObjectSerializer garnetObjectSerializer; public bool StoredProcMode; - public readonly EtagOffsetManagementContext etagOffsetManagementContext; + public byte etagOffsetForVarlen; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, MemoryPool memoryPool, CacheSizeTracker objectStoreSizeTracker, GarnetObjectSerializer garnetObjectSerializer) @@ -30,7 +30,6 @@ public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionM this.memoryPool = memoryPool ?? MemoryPool.Shared; this.objectStoreSizeTracker = objectStoreSizeTracker; this.garnetObjectSerializer = garnetObjectSerializer; - this.etagOffsetManagementContext = new EtagOffsetManagementContext(); } public CustomRawStringFunctions GetCustomCommandFunctions(int id) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index db6e3b6022..2429d9eca6 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -33,16 +33,24 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp // when called withetag all output needs to be placed on the buffer if (input.header.CheckWithEtagFlag()) { - // EXX when unsuccesful will write back NIL + // XX when unsuccesful will write back NIL CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } return false; case RespCommand.SET: case RespCommand.SETEXNX: case RespCommand.SETKEEPTTL: - this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); + if (input.header.CheckWithEtagFlag()) + { + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } + else + { + this.functionsState.etagOffsetForVarlen = 0; + } return true; default: + this.functionsState.etagOffsetForVarlen = 0; if (input.header.cmd > RespCommandExtensions.LastValidCommand) { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); @@ -87,7 +95,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: - int spaceForEtag = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; + int spaceForEtag = this.functionsState.etagOffsetForVarlen; // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -97,8 +105,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB value.ExtraMetadata = input.arg1; newInputValue.CopyTo(value.AsSpan(spaceForEtag)); - // HK TODO maybe cache this calc in etagOffsetManagementContext - if (input.header.CheckWithEtagFlag()) + if (spaceForEtag != 0) { recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag @@ -109,14 +116,14 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB break; case RespCommand.SETKEEPTTL: - spaceForEtag = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; + spaceForEtag = this.functionsState.etagOffsetForVarlen; // Copy input to value, retain metadata in value var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; value.ShrinkSerializedLength(value.MetadataSize + setValue.Length + spaceForEtag); setValue.CopyTo(value.AsSpan(spaceForEtag)); - if (input.header.CheckWithEtagFlag()) + if (spaceForEtag != 0) { recordInfo.SetHasETag(); *(long*)value.ToPointer() = Constants.BaseEtag + 1; @@ -288,12 +295,20 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return false; } - this.functionsState.etagOffsetManagementContext.SetEtagOffsetBasedOnInputHeader(input.header.CheckWithEtagFlag()); - this.functionsState.etagOffsetManagementContext.CalculateOffsets(recordInfo.ETag, ref value); - - int etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; - long oldEtag = this.functionsState.etagOffsetManagementContext.ExistingEtag; + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = Constants.BaseEtag; + this.functionsState.etagOffsetForVarlen = 0; + bool shouldUpdateEtag = recordInfo.ETag; + if (shouldUpdateEtag) + { + // used in varlen + etagIgnoredOffset = Constants.EtagSize; + etagIgnoredEnd = value.LengthWithoutMetadata; + oldEtag = *(long*)value.ToPointer(); + // if something is going to go past this into copy we need to provide offset management for its varlen during allocation + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } RespCommand cmd = input.header.cmd; switch (cmd) @@ -302,7 +317,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (input.header.NotSetGetNorCheckWithEtag()) return true; - // Check if SetGet flag is set if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. @@ -314,7 +328,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // EXX when unsuccesful will write back NIL CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } - // Nothing is set because being in this block means NX was already violated return true; case RespCommand.SETIFMATCH: @@ -361,27 +374,33 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; case RespCommand.SET: case RespCommand.SETEXXX: - // If the user calls withetag then we need to either update an existing etag and set the value - // or set the value with an initial etag and increment it. - // If withEtag is called we return the etag back to the user - - var nextUpdateEtagOffset = etagIgnoredOffset; - var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + // If the user calls withetag then we need to either update an existing etag and set the value or set the value with an etag and increment it. bool inputHeaderHasEtag = input.header.CheckWithEtagFlag(); - if (!inputHeaderHasEtag) - { - // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record - nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - recordInfo.ClearHasETag(); - } - else if (!recordInfo.ETag) + int nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + int nextUpdateEtagOffset = etagIgnoredOffset; + + // only when both are not false && false or true and true, do we need to readjust + if (inputHeaderHasEtag != shouldUpdateEtag) { - // this is the case where we have withetag option and no etag from before - nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; - oldEtag = Constants.BaseEtag; + // in the common path the above condition is skipped + if (inputHeaderHasEtag) + { + // nextUpdate will add etag but currently there is no etag + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; + shouldUpdateEtag = true; + // if something is going to go past this into copy we need to provide offset management for its varlen during allocation + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } + else + { + // nextUpdate will remove etag but currentyly there is an etag + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + this.functionsState.etagOffsetForVarlen = 0; + recordInfo.ClearHasETag(); + } } ArgSlice setValue = input.parseState.GetArgSliceByRef(0); @@ -409,13 +428,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re setValue.ReadOnlySpan.CopyTo(value.AsSpan(nextUpdateEtagOffset)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + // If withEtag is called we return the etag back in the response if (inputHeaderHasEtag) { recordInfo.SetHasETag(); *(long*)value.ToPointer() = oldEtag + 1; // withetag flag means we need to write etag back to the output buffer CopyRespNumber(oldEtag + 1, ref output); - // return since we already updated etag + // early return since we already updated etag return true; } @@ -425,21 +445,32 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // If the user calls withetag then we need to either update an existing etag and set the value // or set the value with an initial etag and increment it. // If withEtag is called we return the etag back to the user - nextUpdateEtagOffset = etagIgnoredOffset; - nextUpdateEtagIgnoredEnd = etagIgnoredEnd; inputHeaderHasEtag = input.header.CheckWithEtagFlag(); - if (!inputHeaderHasEtag) - { - // if the user did not explictly asked for retaining the etag we need to ignore the etag even if it existed on the previous record - nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - recordInfo.ClearHasETag(); - } - else if (!recordInfo.ETag) + + nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + nextUpdateEtagOffset = etagIgnoredOffset; + + // only when both are not false && false or true and true, do we need to readjust + if (inputHeaderHasEtag != shouldUpdateEtag) { - // this is the case where we have withetag option and no etag from before - nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; + // in the common path the above condition is skipped + if (inputHeaderHasEtag) + { + // nextUpdate will add etag but currently there is no etag + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; + shouldUpdateEtag = true; + // if something is going to go past this into copy we need to provide offset management for its varlen during allocation + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } + else + { + // nextUpdate will remove etag but currentyly there is an etag + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + this.functionsState.etagOffsetForVarlen = 0; + recordInfo.ClearHasETag(); + } } setValue = input.parseState.GetArgSliceByRef(0); @@ -744,7 +775,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // increment the Etag transparently if in place update happened - if (recordInfo.ETag) + if (shouldUpdateEtag) { *(long*)value.ToPointer() = oldEtag + 1; } @@ -760,9 +791,16 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB switch (input.header.cmd) { case RespCommand.SETIFMATCH: - int etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; - long existingEtag = this.functionsState.etagOffsetManagementContext.ExistingEtag; + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long existingEtag = Constants.BaseEtag; + if (rmwInfo.RecordInfo.ETag) + { + existingEtag = *(long*)oldValue.ToPointer(); + etagIgnoredOffset = Constants.EtagSize; + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + } + long etagToCheckWith = input.parseState.GetLong(1); if (existingEtag == etagToCheckWith) @@ -790,8 +828,14 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.NotSetGetNorCheckWithEtag()) return false; - etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; - etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; + etagIgnoredOffset = 0; + etagIgnoredEnd = -1; + if (rmwInfo.RecordInfo.ETag) + { + etagIgnoredOffset = Constants.EtagSize; + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + } + if (input.header.CheckSetGetFlag()) { @@ -818,12 +862,12 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.cmd > RespCommandExtensions.LastValidCommand) { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); + etagIgnoredOffset = !rmwInfo.RecordInfo.ETag ? 0 : Constants.EtagSize; var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; - } return true; } @@ -844,10 +888,16 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte RespCommand cmd = input.header.cmd; bool shouldUpdateEtag = recordInfo.ETag; - // offsets are precomputed at InPlaceUpdater - int etagIgnoredOffset = this.functionsState.etagOffsetManagementContext.EtagIgnoredOffset; - int etagIgnoredEnd = this.functionsState.etagOffsetManagementContext.EtagIgnoredEnd; - long oldEtag = this.functionsState.etagOffsetManagementContext.ExistingEtag; + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = Constants.BaseEtag; + + if (shouldUpdateEtag) + { + etagIgnoredOffset = Constants.EtagSize; + etagIgnoredEnd = newValue.LengthWithoutMetadata; + oldEtag = *(long*)oldValue.ToPointer(); + } switch (cmd) { @@ -887,34 +937,25 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; bool inputWithEtag = input.header.CheckWithEtagFlag(); - // Common case - if (!inputWithEtag) - { - // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record - nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - recordInfo.ClearHasETag(); - } - else if (!recordInfo.ETag) + // only when both are not false && false or true and true, do we need to readjust + if (inputWithEtag != shouldUpdateEtag) { - // this is the case where we have withetag option and no etag from before - nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; - recordInfo.SetHasETag(); - shouldUpdateEtag = true; - // removes the need for branching in common path at the end + // in the common path the above condition is skipped if (inputWithEtag) { + // nextUpdate will add etag but currently there is no etag + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = newValue.LengthWithoutMetadata; shouldUpdateEtag = true; - // withetag flag means we need to write etag back to the output buffer - CopyRespNumber(oldEtag + 1, ref output); + recordInfo.SetHasETag(); + } + else + { + // nextUpdate will remove etag but currentyly there is an etag + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + recordInfo.ClearHasETag(); } - } - else - { - shouldUpdateEtag = true; - // removes the need for branching in common path at the end - CopyRespNumber(oldEtag + 1, ref output); } // Check if SetGet flag is set @@ -935,6 +976,11 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte newValue.ExtraMetadata = input.arg1; newInputValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); + if (inputWithEtag) + { + CopyRespNumber(oldEtag + 1, ref output); + } + break; case RespCommand.SETKEEPTTLXX: @@ -942,29 +988,26 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte nextUpdateEtagOffset = etagIgnoredOffset; nextUpdateEtagIgnoredEnd = etagIgnoredEnd; inputWithEtag = input.header.CheckWithEtagFlag(); - if (!inputWithEtag) - { - // if the user did not explictly asked for retaining the etag we need to ignore the etag if it existed on the previous record - nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - } - else if (!recordInfo.ETag) + + // only when both are not false && false or true and true, do we need to readjust + if (inputWithEtag != shouldUpdateEtag) { - // this is the case where we have withetag option and no etag from before - nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = oldValue.LengthWithoutMetadata; - recordInfo.SetHasETag(); + // in the common path the above condition is skipped if (inputWithEtag) { + // nextUpdate will add etag but currently there is no etag + nextUpdateEtagOffset = Constants.EtagSize; + nextUpdateEtagIgnoredEnd = newValue.LengthWithoutMetadata; shouldUpdateEtag = true; - CopyRespNumber(oldEtag + 1, ref output); + recordInfo.SetHasETag(); + } + else + { + // nextUpdate will remove etag but currentyly there is an etag + nextUpdateEtagOffset = 0; + nextUpdateEtagIgnoredEnd = -1; + recordInfo.ClearHasETag(); } - } - else - { - shouldUpdateEtag = true; - // withetag flag means we need to write etag back to the output buffer - CopyRespNumber(oldEtag + 1, ref output); } var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -983,6 +1026,11 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte newValue.ExtraMetadata = oldValue.ExtraMetadata; setValue.CopyTo(newValue.AsSpan(nextUpdateEtagOffset)); + if (inputWithEtag) + { + CopyRespNumber(oldEtag + 1, ref output); + } + break; case RespCommand.EXPIRE: diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index 7aa665acb4..ed8379e1fd 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -116,7 +116,6 @@ public int GetRMWInitialValueLength(ref RawStringInput input) ndigits = NumUtils.NumOfCharInDouble(incrByFloat, out var _, out var _, out var _); return sizeof(int) + ndigits; - default: if (cmd > RespCommandExtensions.LastValidCommand) { @@ -131,7 +130,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + this.functionsState.etagOffsetForVarlen; } } @@ -142,8 +141,7 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) { var cmd = input.header.cmd; - // use the precomputed value - int etagOffset = this.functionsState.etagOffsetManagementContext.EtagOffsetForVarlen; + int etagOffset = this.functionsState.etagOffsetForVarlen; switch (cmd) { From fb4403ad2c8cfe28dbd47fd7fff3d3a113197553 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 7 Jan 2025 15:36:13 -0800 Subject: [PATCH 70/87] WIP --- .../Storage/Functions/MainStore/RMWMethods.cs | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2429d9eca6..38701306ef 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -295,6 +295,41 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return false; } + RespCommand cmd = input.header.cmd; + + // EXPEMENTING: placing this outside of the Switch statement to not let NX command get affected, if this doesnt reduce NX overhead I need to look elsewhere + if (cmd == RespCommand.SETEXNX) + { + if (input.header.NotSetGetNorCheckWithEtag()) + return true; + + if (input.header.CheckSetGetFlag()) + { + int etagAccountedOffset = 0; + int etagAccountedEnd= -1; + long existingEtag = Constants.BaseEtag; + if (recordInfo.ETag) + { + // used in varlen + etagAccountedOffset = Constants.EtagSize; + etagAccountedEnd = value.LengthWithoutMetadata; + existingEtag = *(long*)value.ToPointer(); + // if something is going to go past this into copy we need to provide offset management for its varlen during allocation + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } + // Copy value to output for the GET part of the command. + CopyRespTo(ref value, ref output, etagAccountedOffset, etagAccountedEnd); + } + else + { + // when called withetag all output needs to be placed on the buffer + // EXX when unsuccesful will write back NIL + CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + } + // Nothing is set because being in this block means NX was already violated + return true; + } + int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; long oldEtag = Constants.BaseEtag; @@ -310,26 +345,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re this.functionsState.etagOffsetForVarlen = Constants.EtagSize; } - RespCommand cmd = input.header.cmd; switch (cmd) { case RespCommand.SETEXNX: - if (input.header.NotSetGetNorCheckWithEtag()) - return true; - - if (input.header.CheckSetGetFlag()) - { - // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); - } - else - { - // when called withetag all output needs to be placed on the buffer - // EXX when unsuccesful will write back NIL - CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); - } - // Nothing is set because being in this block means NX was already violated - return true; case RespCommand.SETIFMATCH: long etagFromClient = input.parseState.GetLong(1); From de65bcfcb7d94b0b29ae43554bba576b4c349f7f Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 7 Jan 2025 15:53:37 -0800 Subject: [PATCH 71/87] WIP try rearranging at server session level --- .github/workflows/ci-bdnbenchmark.yml | 2 +- libs/server/Resp/BasicCommands.cs | 48 +++++++++--------- .../Storage/Functions/MainStore/RMWMethods.cs | 50 +++++++------------ 3 files changed, 42 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci-bdnbenchmark.yml b/.github/workflows/ci-bdnbenchmark.yml index 271205673e..7e186a0ecc 100644 --- a/.github/workflows/ci-bdnbenchmark.yml +++ b/.github/workflows/ci-bdnbenchmark.yml @@ -43,7 +43,7 @@ jobs: framework: [ 'net8.0' ] configuration: [ 'Release' ] # temp changes to make my testing loop faster - test: [ 'Operations.RawStringOperations', 'Network.RawStringOperations' ] + test: [ 'Operations.RawStringOperations' ] steps: - name: Check out code uses: actions/checkout@v4 diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index f24fdaa1e6..8295d46bcf 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -865,59 +865,59 @@ private bool NetworkSET_Conditional(RespCommand cmd, int expiry, ref var input = new RawStringInput(cmd, ref parseState, startIdx: 1, arg1: inputArg); - if (getValue || withEtag) + if (!getValue && !withEtag) { - if (withEtag) - input.header.SetWithEtagFlag(); + // the following debug assertion is the catch any edge case leading to SETIFMATCH skipping the above block + Debug.Assert(cmd != RespCommand.SETIFMATCH, "SETIFMATCH should have gone though pointing to right output variable"); - if (getValue) - input.header.SetSetGetFlag(); + GarnetStatus status = storageApi.SET_Conditional(ref key, ref input); - // anything with getValue or withEtag always writes to the buffer in the happy path - SpanByteAndMemory outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - GarnetStatus status = storageApi.SET_Conditional(ref key, - ref input, ref outputBuffer); + bool ok = status != GarnetStatus.NOTFOUND; - // The data will be on the buffer either when we know the response is ok or when the withEtag flag is set. - bool ok = status != GarnetStatus.NOTFOUND || withEtag; + if (cmd == RespCommand.SETEXNX) + ok = !ok; if (ok) { - if (!outputBuffer.IsSpanByte) - SendAndReset(outputBuffer.Memory, outputBuffer.Length); - else - dcurr += outputBuffer.Length; + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) + SendAndReset(); } else { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); } - return true; } else { - // the following debug assertion is the catch any edge case leading to SETIFMATCH skipping the above block - Debug.Assert(cmd != RespCommand.SETIFMATCH, "SETIFMATCH should have gone though pointing to right output variable"); + if (withEtag) + input.header.SetWithEtagFlag(); - GarnetStatus status = storageApi.SET_Conditional(ref key, ref input); + if (getValue) + input.header.SetSetGetFlag(); - bool ok = status != GarnetStatus.NOTFOUND; + // anything with getValue or withEtag always writes to the buffer in the happy path + SpanByteAndMemory outputBuffer = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + GarnetStatus status = storageApi.SET_Conditional(ref key, + ref input, ref outputBuffer); - if (cmd == RespCommand.SETEXNX) - ok = !ok; + // The data will be on the buffer either when we know the response is ok or when the withEtag flag is set. + bool ok = status != GarnetStatus.NOTFOUND || withEtag; if (ok) { - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_OK, ref dcurr, dend)) - SendAndReset(); + if (!outputBuffer.IsSpanByte) + SendAndReset(outputBuffer.Memory, outputBuffer.Length); + else + dcurr += outputBuffer.Length; } else { while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); } + return true; } } diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 38701306ef..b0c577169b 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -297,39 +297,6 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; - // EXPEMENTING: placing this outside of the Switch statement to not let NX command get affected, if this doesnt reduce NX overhead I need to look elsewhere - if (cmd == RespCommand.SETEXNX) - { - if (input.header.NotSetGetNorCheckWithEtag()) - return true; - - if (input.header.CheckSetGetFlag()) - { - int etagAccountedOffset = 0; - int etagAccountedEnd= -1; - long existingEtag = Constants.BaseEtag; - if (recordInfo.ETag) - { - // used in varlen - etagAccountedOffset = Constants.EtagSize; - etagAccountedEnd = value.LengthWithoutMetadata; - existingEtag = *(long*)value.ToPointer(); - // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; - } - // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, etagAccountedOffset, etagAccountedEnd); - } - else - { - // when called withetag all output needs to be placed on the buffer - // EXX when unsuccesful will write back NIL - CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); - } - // Nothing is set because being in this block means NX was already violated - return true; - } - int etagIgnoredOffset = 0; int etagIgnoredEnd = -1; long oldEtag = Constants.BaseEtag; @@ -348,6 +315,23 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re switch (cmd) { case RespCommand.SETEXNX: + if (input.header.NotSetGetNorCheckWithEtag()) + return true; + + if (input.header.CheckSetGetFlag()) + { + // Copy value to output for the GET part of the command. + CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + } + else + { + // when called withetag all output needs to be placed on the buffer + // EXX when unsuccesful will write back NIL + CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + } + + // Nothing is set because being in this block means NX was already violated + return true; case RespCommand.SETIFMATCH: long etagFromClient = input.parseState.GetLong(1); From eee23e0082f499b0af342200ef77ecd5a5b41ee5 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 7 Jan 2025 16:35:08 -0800 Subject: [PATCH 72/87] reduce branching in rmw --- .../Storage/Functions/FunctionsState.cs | 3 ++ .../Storage/Functions/MainStore/RMWMethods.cs | 49 +++++-------------- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index 3f75850468..c74bcb3858 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -20,6 +20,9 @@ internal sealed class FunctionsState public readonly GarnetObjectSerializer garnetObjectSerializer; public bool StoredProcMode; public byte etagOffsetForVarlen; + public int etagIgnoredOffset; + public int etagIgnoredEnd; + public long oldEtag; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, MemoryPool memoryPool, CacheSizeTracker objectStoreSizeTracker, GarnetObjectSerializer garnetObjectSerializer) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index b0c577169b..35e9d47527 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -297,17 +297,17 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; + int etagIgnoredOffset = this.functionsState.etagIgnoredOffset = 0; + int etagIgnoredEnd = this.functionsState.etagIgnoredEnd = -1; + long oldEtag = this.functionsState.oldEtag = Constants.BaseEtag; this.functionsState.etagOffsetForVarlen = 0; bool shouldUpdateEtag = recordInfo.ETag; if (shouldUpdateEtag) { // used in varlen - etagIgnoredOffset = Constants.EtagSize; + etagIgnoredOffset = this.functionsState.etagIgnoredOffset = Constants.EtagSize; etagIgnoredEnd = value.LengthWithoutMetadata; - oldEtag = *(long*)value.ToPointer(); + oldEtag = this.functionsState.oldEtag = *(long*)value.ToPointer(); // if something is going to go past this into copy we need to provide offset management for its varlen during allocation this.functionsState.etagOffsetForVarlen = Constants.EtagSize; } @@ -793,15 +793,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB switch (input.header.cmd) { case RespCommand.SETIFMATCH: - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long existingEtag = Constants.BaseEtag; - if (rmwInfo.RecordInfo.ETag) - { - existingEtag = *(long*)oldValue.ToPointer(); - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - } + long existingEtag = this.functionsState.oldEtag; long etagToCheckWith = input.parseState.GetLong(1); @@ -811,7 +803,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB } else { - CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); + CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, this.functionsState.etagIgnoredOffset, this.functionsState.etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); return false; } @@ -830,19 +822,10 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.NotSetGetNorCheckWithEtag()) return false; - etagIgnoredOffset = 0; - etagIgnoredEnd = -1; - if (rmwInfo.RecordInfo.ETag) - { - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - } - - if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output, this.functionsState.etagIgnoredOffset, this.functionsState.etagIgnoredEnd); } else if (input.header.CheckWithEtagFlag()) { @@ -864,9 +847,8 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.cmd > RespCommandExtensions.LastValidCommand) { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); - etagIgnoredOffset = !rmwInfo.RecordInfo.ETag ? 0 : Constants.EtagSize; var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(this.functionsState.etagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -890,16 +872,9 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte RespCommand cmd = input.header.cmd; bool shouldUpdateEtag = recordInfo.ETag; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; - - if (shouldUpdateEtag) - { - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = newValue.LengthWithoutMetadata; - oldEtag = *(long*)oldValue.ToPointer(); - } + int etagIgnoredOffset = this.functionsState.etagIgnoredOffset; + int etagIgnoredEnd = this.functionsState.etagIgnoredEnd; + long oldEtag = this.functionsState.oldEtag; switch (cmd) { From de179ce142823bf09145c385a9de223ed174a121 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 7 Jan 2025 17:02:42 -0800 Subject: [PATCH 73/87] add back bdn benchmarks --- .github/workflows/ci-bdnbenchmark.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci-bdnbenchmark.yml b/.github/workflows/ci-bdnbenchmark.yml index 7e186a0ecc..7ca620bc73 100644 --- a/.github/workflows/ci-bdnbenchmark.yml +++ b/.github/workflows/ci-bdnbenchmark.yml @@ -42,8 +42,7 @@ jobs: os: [ ubuntu-latest, windows-latest ] framework: [ 'net8.0' ] configuration: [ 'Release' ] - # temp changes to make my testing loop faster - test: [ 'Operations.RawStringOperations' ] + test: [ 'Operations.BasicOperations', 'Operations.ObjectOperations', 'Operations.HashObjectOperations', 'Cluster.ClusterMigrate', 'Cluster.ClusterOperations', 'Lua.LuaScripts', 'Operations.CustomOperations', 'Operations.RawStringOperations', 'Operations.ScriptOperations','Network.BasicOperations', 'Network.RawStringOperations' ] steps: - name: Check out code uses: actions/checkout@v4 From bc21fc49181abf68f37dc366b41d2ff61159321a Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Tue, 7 Jan 2025 18:00:39 -0800 Subject: [PATCH 74/87] go back to using branching again --- .../Storage/Functions/FunctionsState.cs | 3 - .../Storage/Functions/MainStore/RMWMethods.cs | 58 ++++++++++++------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index c74bcb3858..3f75850468 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -20,9 +20,6 @@ internal sealed class FunctionsState public readonly GarnetObjectSerializer garnetObjectSerializer; public bool StoredProcMode; public byte etagOffsetForVarlen; - public int etagIgnoredOffset; - public int etagIgnoredEnd; - public long oldEtag; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, MemoryPool memoryPool, CacheSizeTracker objectStoreSizeTracker, GarnetObjectSerializer garnetObjectSerializer) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 35e9d47527..2a03ce4ae9 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -40,14 +40,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.SET: case RespCommand.SETEXNX: case RespCommand.SETKEEPTTL: - if (input.header.CheckWithEtagFlag()) - { - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; - } - else - { - this.functionsState.etagOffsetForVarlen = 0; - } + this.functionsState.etagOffsetForVarlen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); return true; default: this.functionsState.etagOffsetForVarlen = 0; @@ -297,17 +290,17 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; - int etagIgnoredOffset = this.functionsState.etagIgnoredOffset = 0; - int etagIgnoredEnd = this.functionsState.etagIgnoredEnd = -1; - long oldEtag = this.functionsState.oldEtag = Constants.BaseEtag; + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = Constants.BaseEtag; this.functionsState.etagOffsetForVarlen = 0; bool shouldUpdateEtag = recordInfo.ETag; if (shouldUpdateEtag) { // used in varlen - etagIgnoredOffset = this.functionsState.etagIgnoredOffset = Constants.EtagSize; + etagIgnoredOffset = Constants.EtagSize; etagIgnoredEnd = value.LengthWithoutMetadata; - oldEtag = this.functionsState.oldEtag = *(long*)value.ToPointer(); + oldEtag = *(long*)value.ToPointer(); // if something is going to go past this into copy we need to provide offset management for its varlen during allocation this.functionsState.etagOffsetForVarlen = Constants.EtagSize; } @@ -788,12 +781,25 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { - // offsets are precomputed at inplace updater + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = Constants.BaseEtag; + this.functionsState.etagOffsetForVarlen = 0; + bool shouldUpdateEtag = rmwInfo.RecordInfo.ETag; + if (shouldUpdateEtag) + { + // used in varlen + etagIgnoredOffset = Constants.EtagSize; + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + oldEtag = *(long*)oldValue.ToPointer(); + // if something is going to go past this into copy we need to provide offset management for its varlen during allocation + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } switch (input.header.cmd) { case RespCommand.SETIFMATCH: - long existingEtag = this.functionsState.oldEtag; + long existingEtag = oldEtag; long etagToCheckWith = input.parseState.GetLong(1); @@ -803,7 +809,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB } else { - CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, this.functionsState.etagIgnoredOffset, this.functionsState.etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); + CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); return false; } @@ -825,7 +831,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, this.functionsState.etagIgnoredOffset, this.functionsState.etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); } else if (input.header.CheckWithEtagFlag()) { @@ -848,7 +854,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(this.functionsState.etagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -872,9 +878,19 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte RespCommand cmd = input.header.cmd; bool shouldUpdateEtag = recordInfo.ETag; - int etagIgnoredOffset = this.functionsState.etagIgnoredOffset; - int etagIgnoredEnd = this.functionsState.etagIgnoredEnd; - long oldEtag = this.functionsState.oldEtag; + int etagIgnoredOffset = 0; + int etagIgnoredEnd = -1; + long oldEtag = Constants.BaseEtag; + this.functionsState.etagOffsetForVarlen = 0; + if (shouldUpdateEtag) + { + // used in varlen + etagIgnoredOffset = Constants.EtagSize; + etagIgnoredEnd = oldValue.LengthWithoutMetadata; + oldEtag = *(long*)oldValue.ToPointer(); + // if something is going to go past this into copy we need to provide offset management for its varlen during allocation + this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + } switch (cmd) { From adabc24fb9dbc7dabc2112022505ccd3f4166a2e Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 8 Jan 2025 21:27:27 -0800 Subject: [PATCH 75/87] Big ole refactor --- libs/server/AOF/AofProcessor.cs | 1 + libs/server/Constants.cs | 2 +- libs/server/Resp/BasicCommands.cs | 169 ------------ libs/server/Resp/BasicEtagCommands.cs | 187 +++++++++++++ libs/server/Resp/RespServerSession.cs | 1 + libs/server/Storage/Functions/EtagState.cs | 56 ++++ .../Storage/Functions/FunctionsState.cs | 4 +- .../Functions/MainStore/DeleteMethods.cs | 2 + .../Functions/MainStore/PrivateMethods.cs | 82 +++--- .../Storage/Functions/MainStore/RMWMethods.cs | 251 ++++++++---------- .../Functions/MainStore/ReadMethods.cs | 102 ++++--- .../Functions/MainStore/UpsertMethods.cs | 2 + .../Functions/MainStore/VarLenInputMethods.cs | 24 +- .../Tsavorite/cs/src/core/VarLen/SpanByte.cs | 13 + test/Garnet.test/RespAofTests.cs | 4 + test/Garnet.test/RespEtagTests.cs | 2 +- 16 files changed, 487 insertions(+), 415 deletions(-) create mode 100644 libs/server/Resp/BasicEtagCommands.cs create mode 100644 libs/server/Storage/Functions/EtagState.cs diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index 94bf35913c..e73d3cb4bb 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -134,6 +134,7 @@ private unsafe void RecoverReplay(long untilAddress) { count++; ProcessAofRecord(entry, length); + this.respServerSession.storageSession.functionsState.etagState.ResetToDefaultVals(); if (count % 100_000 == 0) logger?.LogInformation("Completed AOF replay of {count} records, until AOF address {nextAofAddress}", count, nextAofAddress); } diff --git a/libs/server/Constants.cs b/libs/server/Constants.cs index 6cd2d14674..7337b6a714 100644 --- a/libs/server/Constants.cs +++ b/libs/server/Constants.cs @@ -8,6 +8,6 @@ internal static class Constants { public const byte EtagSize = sizeof(long); - public const int BaseEtag = 0; + public const long BaseEtag = 0; } } \ No newline at end of file diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index 8295d46bcf..c7dccbf276 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Garnet.common; @@ -311,174 +310,6 @@ private bool NetworkGETSET(ref TGarnetApi storageApi) return NetworkSETEXNX(ref storageApi); } - /// - /// GETWITHETAG key - /// Given a key get the value and ETag - /// - private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) - where TGarnetApi : IGarnetApi - { - Debug.Assert(parseState.Count == 1); - - var key = parseState.GetArgSliceByRef(0).SpanByte; - var input = new RawStringInput(RespCommand.GETWITHETAG, ref parseState, startIdx: 1); - var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - var status = storageApi.GET(ref key, ref input, ref output); - - switch (status) - { - case GarnetStatus.NOTFOUND: - Debug.Assert(output.IsSpanByte); - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) - SendAndReset(); - break; - default: - if (!output.IsSpanByte) - SendAndReset(output.Memory, output.Length); - else - dcurr += output.Length; - break; - } - - return true; - } - - /// - /// GETIFNOTMATCH key etag - /// Given a key and an etag, return the value and it's etag only if the sent ETag does not match the existing ETag. - /// - private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) - where TGarnetApi : IGarnetApi - { - Debug.Assert(parseState.Count == 2); - - var key = parseState.GetArgSliceByRef(0).SpanByte; - var input = new RawStringInput(RespCommand.GETIFNOTMATCH, ref parseState, startIdx: 1); - var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); - var status = storageApi.GET(ref key, ref input, ref output); - - switch (status) - { - case GarnetStatus.NOTFOUND: - Debug.Assert(output.IsSpanByte); - while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) - SendAndReset(); - break; - default: - if (!output.IsSpanByte) - SendAndReset(output.Memory, output.Length); - else - dcurr += output.Length; - break; - } - - return true; - } - - /// - /// SETIFMATCH key val etag EX|PX expiry - /// Sets a key value pair only if an already existing etag does not match the etag sent as a part of the request - /// - /// - /// - /// - private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) - where TGarnetApi : IGarnetApi - { - if (parseState.Count < 3 || parseState.Count > 5) - { - return AbortWithWrongNumberOfArguments(nameof(RespCommand.SETIFMATCH)); - } - - // SETIFMATCH Args: KEY VAL ETAG -> [ ((EX || PX) expiration)] - int expiry = 0; - ReadOnlySpan errorMessage = default; - var expOption = ExpirationOption.None; - - var tokenIdx = 3; - Span nextOpt = default; - var optUpperCased = false; - while (tokenIdx < parseState.Count || optUpperCased) - { - if (!optUpperCased) - { - nextOpt = parseState.GetArgSliceByRef(tokenIdx++).Span; - } - - if (nextOpt.SequenceEqual(CmdStrings.EX)) - { - // Validate expiry - if (!parseState.TryGetInt(tokenIdx++, out expiry)) - { - errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; - break; - } - - if (expOption != ExpirationOption.None) - { - errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; - break; - } - - expOption = ExpirationOption.EX; - if (expiry <= 0) - { - errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET; - break; - } - } - else if (nextOpt.SequenceEqual(CmdStrings.PX)) - { - // Validate expiry - if (!parseState.TryGetInt(tokenIdx++, out expiry)) - { - errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; - break; - } - - if (expOption != ExpirationOption.None) - { - errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; - break; - } - - expOption = ExpirationOption.PX; - if (expiry <= 0) - { - errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET; - break; - } - } - else - { - if (!optUpperCased) - { - AsciiUtils.ToUpperInPlace(nextOpt); - optUpperCased = true; - continue; - } - - errorMessage = CmdStrings.RESP_ERR_GENERIC_UNK_CMD; - break; - } - - optUpperCased = false; - } - - if (!errorMessage.IsEmpty) - { - while (!RespWriteUtils.WriteError(errorMessage, ref dcurr, dend)) - SendAndReset(); - return true; - } - - SpanByte key = parseState.GetArgSliceByRef(0).SpanByte; - - NetworkSET_Conditional(RespCommand.SETIFMATCH, expiry, ref key, getValue: true, highPrecision: expOption == ExpirationOption.PX, withEtag: true, ref storageApi); - - return true; - } - /// /// SETRANGE /// diff --git a/libs/server/Resp/BasicEtagCommands.cs b/libs/server/Resp/BasicEtagCommands.cs new file mode 100644 index 0000000000..607fd27d46 --- /dev/null +++ b/libs/server/Resp/BasicEtagCommands.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; +using Garnet.common; +using Microsoft.Extensions.Logging; +using Tsavorite.core; + +namespace Garnet.server +{ + /// + /// Server session for RESP protocol - ETag associated commands are in this file + /// + internal sealed unsafe partial class RespServerSession : ServerSessionBase + { + /// + /// GETWITHETAG key + /// Given a key get the value and it's ETag + /// + private bool NetworkGETWITHETAG(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + Debug.Assert(parseState.Count == 1); + + var key = parseState.GetArgSliceByRef(0).SpanByte; + var input = new RawStringInput(RespCommand.GETWITHETAG, ref parseState, startIdx: 1); + var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + var status = storageApi.GET(ref key, ref input, ref output); + + switch (status) + { + case GarnetStatus.NOTFOUND: + Debug.Assert(output.IsSpanByte); + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + default: + if (!output.IsSpanByte) + SendAndReset(output.Memory, output.Length); + else + dcurr += output.Length; + break; + } + + return true; + } + + /// + /// GETIFNOTMATCH key etag + /// Given a key and an etag, return the value and it's etag. + /// + private bool NetworkGETIFNOTMATCH(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + Debug.Assert(parseState.Count == 2); + + var key = parseState.GetArgSliceByRef(0).SpanByte; + var input = new RawStringInput(RespCommand.GETIFNOTMATCH, ref parseState, startIdx: 1); + var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); + var status = storageApi.GET(ref key, ref input, ref output); + + switch (status) + { + case GarnetStatus.NOTFOUND: + Debug.Assert(output.IsSpanByte); + while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) + SendAndReset(); + break; + default: + if (!output.IsSpanByte) + SendAndReset(output.Memory, output.Length); + else + dcurr += output.Length; + break; + } + + return true; + } + + /// + /// SETIFMATCH key val etag EX|PX expiry + /// Sets a key value pair only if the already existing etag matches the etag sent as a part of the request. + /// + /// + /// + /// + private bool NetworkSETIFMATCH(ref TGarnetApi storageApi) + where TGarnetApi : IGarnetApi + { + if (parseState.Count < 3 || parseState.Count > 5) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.SETIFMATCH)); + } + + // SETIFMATCH Args: KEY VAL ETAG -> [ ((EX || PX) expiration)] + int expiry = 0; + ReadOnlySpan errorMessage = default; + var expOption = ExpirationOption.None; + + var tokenIdx = 3; + Span nextOpt = default; + var optUpperCased = false; + while (tokenIdx < parseState.Count || optUpperCased) + { + if (!optUpperCased) + { + nextOpt = parseState.GetArgSliceByRef(tokenIdx++).Span; + } + + if (nextOpt.SequenceEqual(CmdStrings.EX)) + { + // Validate expiry + if (!parseState.TryGetInt(tokenIdx++, out expiry)) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + break; + } + + if (expOption != ExpirationOption.None) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; + break; + } + + expOption = ExpirationOption.EX; + if (expiry <= 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET; + break; + } + } + else if (nextOpt.SequenceEqual(CmdStrings.PX)) + { + // Validate expiry + if (!parseState.TryGetInt(tokenIdx++, out expiry)) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; + break; + } + + if (expOption != ExpirationOption.None) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR; + break; + } + + expOption = ExpirationOption.PX; + if (expiry <= 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_INVALIDEXP_IN_SET; + break; + } + } + else + { + if (!optUpperCased) + { + AsciiUtils.ToUpperInPlace(nextOpt); + optUpperCased = true; + continue; + } + + errorMessage = CmdStrings.RESP_ERR_GENERIC_UNK_CMD; + break; + } + + optUpperCased = false; + } + + if (!errorMessage.IsEmpty) + { + while (!RespWriteUtils.WriteError(errorMessage, ref dcurr, dend)) + SendAndReset(); + return true; + } + + SpanByte key = parseState.GetArgSliceByRef(0).SpanByte; + + NetworkSET_Conditional(RespCommand.SETIFMATCH, expiry, ref key, getValue: true, highPrecision: expOption == ExpirationOption.PX, withEtag: true, ref storageApi); + + return true; + } + } +} \ No newline at end of file diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index 7a9ba3b4e0..d71ae46437 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -408,6 +408,7 @@ private void ProcessMessages() while (bytesRead - readHead >= 4) { + storageSession.functionsState.etagState.ResetToDefaultVals(); // First, parse the command, making sure we have the entire command available // We use endReadHead to track the end of the current command // On success, readHead is left at the start of the command payload for legacy operators diff --git a/libs/server/Storage/Functions/EtagState.cs b/libs/server/Storage/Functions/EtagState.cs new file mode 100644 index 0000000000..f9f0694be6 --- /dev/null +++ b/libs/server/Storage/Functions/EtagState.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Tsavorite.core; + +namespace Garnet.server +{ + /// + /// Indirection wrapper to provide a way to set offsets related to Etags and use the getters opaquely from outside. + /// + public class EtagState + { + /// + /// Offset used accounting space for an etag during allocation + /// + public byte etagOffsetForVarlen { get; set; } = 0; + + /// + /// Gives an offset used to opaquely work with Etag in a payload. By calling this you can skip past the etag if it is present. + /// + public byte etagSkippedStart { get; private set; } = 0; + + /// + /// Resp response methods depend on the value for end being -1 or length of the payload. This field lets you work with providing the end opaquely. + /// + public int etagAccountedLength { get; private set; } = -1; + + /// + /// Field provides access to getting an Etag from a record, hiding whether it is actually present or not. + /// + public long etag { get; private set; } = Constants.BaseEtag; + + /// + /// Sets the values to indicate the presence of an Etag as a part of the payload value + /// + /// The SpanByte for the record + public void SetValsForRecordWithEtag(ref SpanByte value) + { + etagOffsetForVarlen = Constants.EtagSize; + etagSkippedStart = Constants.EtagSize; + etagAccountedLength = value.LengthWithoutMetadata; + etag = value.GetEtagInPayload(); + } + + /// + /// Resets the values back to default values so that state between operations does not leak + /// + public void ResetToDefaultVals() + { + etagOffsetForVarlen = 0; + etagSkippedStart = 0; + etagAccountedLength = -1; + etag = Constants.BaseEtag; + } + } +} \ No newline at end of file diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index 3f75850468..6b5f56e437 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Buffers; +using System.Net.Http; using Tsavorite.core; namespace Garnet.server @@ -18,8 +19,8 @@ internal sealed class FunctionsState public readonly MemoryPool memoryPool; public readonly CacheSizeTracker objectStoreSizeTracker; public readonly GarnetObjectSerializer garnetObjectSerializer; + public EtagState etagState; public bool StoredProcMode; - public byte etagOffsetForVarlen; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, MemoryPool memoryPool, CacheSizeTracker objectStoreSizeTracker, GarnetObjectSerializer garnetObjectSerializer) @@ -30,6 +31,7 @@ public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionM this.memoryPool = memoryPool ?? MemoryPool.Shared; this.objectStoreSizeTracker = objectStoreSizeTracker; this.garnetObjectSerializer = garnetObjectSerializer; + this.etagState = new EtagState(); } public CustomRawStringFunctions GetCustomCommandFunctions(int id) diff --git a/libs/server/Storage/Functions/MainStore/DeleteMethods.cs b/libs/server/Storage/Functions/MainStore/DeleteMethods.cs index d94aa8b7eb..6c055bd368 100644 --- a/libs/server/Storage/Functions/MainStore/DeleteMethods.cs +++ b/libs/server/Storage/Functions/MainStore/DeleteMethods.cs @@ -13,6 +13,7 @@ namespace Garnet.server /// public bool SingleDeleter(ref SpanByte key, ref SpanByte value, ref DeleteInfo deleteInfo, ref RecordInfo recordInfo) { + recordInfo.ClearHasETag(); functionsState.watchVersionMap.IncrementVersion(deleteInfo.KeyHash); return true; } @@ -27,6 +28,7 @@ public void PostSingleDeleter(ref SpanByte key, ref DeleteInfo deleteInfo) /// public bool ConcurrentDeleter(ref SpanByte key, ref SpanByte value, ref DeleteInfo deleteInfo, ref RecordInfo recordInfo) { + recordInfo.ClearHasETag(); if (!deleteInfo.RecordInfo.Modified) functionsState.watchVersionMap.IncrementVersion(deleteInfo.KeyHash); if (functionsState.appendOnlyFile != null) diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 21512e17a4..02ef1f0878 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -83,7 +83,7 @@ void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int } } - void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending, int payloadEtagAccountedEndOffset, int etagAccountedEnd, bool hasEtagInVal) + void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, bool isFromPending) { switch (input.header.cmd) { @@ -93,7 +93,7 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB // This is accomplished by calling ConvertToHeap on the destination SpanByteAndMemory if (isFromPending) dst.ConvertToHeap(); - CopyRespTo(ref value, ref dst, payloadEtagAccountedEndOffset, etagAccountedEnd); + CopyRespTo(ref value, ref dst, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); break; case RespCommand.MIGRATE: @@ -123,20 +123,20 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB // Get value without RESP header; exclude expiration if (value.LengthWithoutMetadata <= dst.Length) { - dst.Length = value.LengthWithoutMetadata - payloadEtagAccountedEndOffset; - value.AsReadOnlySpan(payloadEtagAccountedEndOffset).CopyTo(dst.SpanByte.AsSpan()); + dst.Length = value.LengthWithoutMetadata - functionsState.etagState.etagSkippedStart; + value.AsReadOnlySpan(functionsState.etagState.etagSkippedStart).CopyTo(dst.SpanByte.AsSpan()); return; } dst.ConvertToHeap(); - dst.Length = value.LengthWithoutMetadata - payloadEtagAccountedEndOffset; + dst.Length = value.LengthWithoutMetadata - functionsState.etagState.etagSkippedStart; dst.Memory = functionsState.memoryPool.Rent(value.LengthWithoutMetadata); - value.AsReadOnlySpan(payloadEtagAccountedEndOffset).CopyTo(dst.Memory.Memory.Span); + value.AsReadOnlySpan(functionsState.etagState.etagSkippedStart).CopyTo(dst.Memory.Memory.Span); break; case RespCommand.GETBIT: var offset = input.parseState.GetLong(0); - var oldValSet = BitmapManager.GetBit(offset, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); + var oldValSet = BitmapManager.GetBit(offset, value.ToPointer() + functionsState.etagState.etagSkippedStart, value.Length - functionsState.etagState.etagSkippedStart); if (oldValSet == 0) CopyDefaultResp(CmdStrings.RESP_RETURN_VAL_0, ref dst); else @@ -160,7 +160,7 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB } } - var count = BitmapManager.BitCountDriver(bcStartOffset, bcEndOffset, bcOffsetType, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); + var count = BitmapManager.BitCountDriver(bcStartOffset, bcEndOffset, bcOffsetType, value.ToPointer() + functionsState.etagState.etagSkippedStart, value.Length - functionsState.etagState.etagSkippedStart); CopyRespNumber(count, ref dst); break; @@ -186,13 +186,13 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB } var pos = BitmapManager.BitPosDriver(bpSetVal, bpStartOffset, bpEndOffset, bpOffsetType, - value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); + value.ToPointer() + functionsState.etagState.etagSkippedStart, value.Length - functionsState.etagState.etagSkippedStart); *(long*)dst.SpanByte.ToPointer() = pos; CopyRespNumber(pos, ref dst); break; case RespCommand.BITOP: - var bitmap = (IntPtr)value.ToPointer() + payloadEtagAccountedEndOffset; + var bitmap = (IntPtr)value.ToPointer() + functionsState.etagState.etagSkippedStart; var output = dst.SpanByte.ToPointer(); *(long*)output = bitmap.ToInt64(); @@ -202,7 +202,7 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); - var (retValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); + var (retValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, value.ToPointer() + functionsState.etagState.etagSkippedStart, value.Length - functionsState.etagState.etagSkippedStart); if (!overflow) CopyRespNumber(retValue, ref dst); else @@ -211,16 +211,16 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB case RespCommand.PFCOUNT: case RespCommand.PFMERGE: - if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer() + payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset)) + if (!HyperLogLog.DefaultHLL.IsValidHYLL(value.ToPointer(), value.Length)) { *(long*)dst.SpanByte.ToPointer() = -1; return; } - if (value.Length - payloadEtagAccountedEndOffset <= dst.Length) + if (value.Length <= dst.Length) { - Buffer.MemoryCopy(value.ToPointer() + payloadEtagAccountedEndOffset, dst.SpanByte.ToPointer(), value.Length - payloadEtagAccountedEndOffset, value.Length - payloadEtagAccountedEndOffset); - dst.SpanByte.Length = value.Length - payloadEtagAccountedEndOffset; + Buffer.MemoryCopy(value.ToPointer(), dst.SpanByte.ToPointer(), value.Length, value.Length); + dst.SpanByte.Length = value.Length; return; } @@ -237,37 +237,13 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB return; case RespCommand.GETRANGE: - var len = value.LengthWithoutMetadata - payloadEtagAccountedEndOffset; + var len = value.LengthWithoutMetadata - functionsState.etagState.etagSkippedStart; var start = input.parseState.GetInt(0); var end = input.parseState.GetInt(1); (start, end) = NormalizeRange(start, end, len); - CopyRespTo(ref value, ref dst, start + payloadEtagAccountedEndOffset, end + payloadEtagAccountedEndOffset); + CopyRespTo(ref value, ref dst, start + functionsState.etagState.etagSkippedStart, end + functionsState.etagState.etagSkippedStart); return; - case RespCommand.SETIFMATCH: - case RespCommand.GETIFNOTMATCH: - case RespCommand.GETWITHETAG: - int valueLength = value.LengthWithoutMetadata; - // always writing an array of size 2 => *2\r\n - int desiredLength = 4; - ReadOnlySpan etagTruncatedVal; - // get etag to write, default etag 0 for when no etag - long etag = hasEtagInVal ? *(long*)value.ToPointer() : Constants.BaseEtag; - // remove the length of the ETAG - var etagAccountedValueLength = valueLength - payloadEtagAccountedEndOffset; - if (hasEtagInVal) - { - etagAccountedValueLength = valueLength - Constants.EtagSize; - payloadEtagAccountedEndOffset = Constants.EtagSize; - } - // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below - etagTruncatedVal = value.AsReadOnlySpan(payloadEtagAccountedEndOffset); - // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n - desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; - - WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); - return; - case RespCommand.EXPIRETIME: var expireTime = ConvertUtils.UnixTimeInSecondsFromTicks(value.MetadataSize > 0 ? value.ExtraMetadata : -1); CopyRespNumber(expireTime, ref dst); @@ -283,6 +259,29 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB } } + void CopyRespWithEtagData(ref SpanByte value, ref SpanByteAndMemory dst, bool hasEtagInVal) + { + int valueLength = value.LengthWithoutMetadata; + // always writing an array of size 2 => *2\r\n + int desiredLength = 4; + ReadOnlySpan etagTruncatedVal; + // get etag to write, default etag 0 for when no etag + long etag = hasEtagInVal ? value.GetEtagInPayload() : Constants.BaseEtag; + // remove the length of the ETAG + var etagAccountedValueLength = valueLength - functionsState.etagState.etagSkippedStart; + if (hasEtagInVal) + { + etagAccountedValueLength = valueLength - Constants.EtagSize; + } + + // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below + etagTruncatedVal = value.AsReadOnlySpan(functionsState.etagState.etagSkippedStart); + // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n + desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; + + WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); + } + void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst, bool writeDirect = false) { if (desiredLength <= dst.Length) @@ -460,7 +459,6 @@ void EvaluateExpireCopyUpdate(ExpireOption optionType, bool expiryExists, long n internal static bool CheckExpiry(ref SpanByte src) => src.ExtraMetadata < DateTimeOffset.UtcNow.Ticks; - // HK TODO: SUSPECT INCY IS JUST GETTING SKIPPED static bool InPlaceUpdateNumber(long val, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo, ref RecordInfo recordInfo, int valueOffset) { var fNeg = false; diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2a03ce4ae9..8944db1dd3 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -40,10 +40,9 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.SET: case RespCommand.SETEXNX: case RespCommand.SETKEEPTTL: - this.functionsState.etagOffsetForVarlen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); + this.functionsState.etagState.etagOffsetForVarlen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); return true; default: - this.functionsState.etagOffsetForVarlen = 0; if (input.header.cmd > RespCommandExtensions.LastValidCommand) { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); @@ -88,8 +87,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB case RespCommand.SET: case RespCommand.SETEXNX: - int spaceForEtag = this.functionsState.etagOffsetForVarlen; - + int spaceForEtag = this.functionsState.etagState.etagOffsetForVarlen; // Copy input to value var newInputValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; var metadataSize = input.arg1 == 0 ? 0 : sizeof(long); @@ -102,15 +100,15 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB { recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag - *(long*)value.ToPointer() = Constants.BaseEtag + 1; + value.SetEtagInPayload(Constants.BaseEtag + 1); + functionsState.etagState.SetValsForRecordWithEtag(ref value); // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX CopyRespNumber(Constants.BaseEtag + 1, ref output); } break; case RespCommand.SETKEEPTTL: - spaceForEtag = this.functionsState.etagOffsetForVarlen; - + spaceForEtag = this.functionsState.etagState.etagOffsetForVarlen; // Copy input to value, retain metadata in value var setValue = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; value.ShrinkSerializedLength(value.MetadataSize + setValue.Length + spaceForEtag); @@ -119,7 +117,8 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB if (spaceForEtag != 0) { recordInfo.SetHasETag(); - *(long*)value.ToPointer() = Constants.BaseEtag + 1; + value.SetEtagInPayload(Constants.BaseEtag + 1); + functionsState.etagState.SetValsForRecordWithEtag(ref value); // Copy initial etag to output CopyRespNumber(Constants.BaseEtag + 1, ref output); } @@ -290,19 +289,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re RespCommand cmd = input.header.cmd; - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; - this.functionsState.etagOffsetForVarlen = 0; - bool shouldUpdateEtag = recordInfo.ETag; - if (shouldUpdateEtag) + bool hasEtagInRecord = recordInfo.ETag; + if (hasEtagInRecord) { - // used in varlen - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = value.LengthWithoutMetadata; - oldEtag = *(long*)value.ToPointer(); - // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + functionsState.etagState.SetValsForRecordWithEtag(ref value); } switch (cmd) @@ -314,7 +304,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output); } else { @@ -327,10 +317,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; case RespCommand.SETIFMATCH: long etagFromClient = input.parseState.GetLong(1); - - if (oldEtag != etagFromClient) + if (functionsState.etagState.etag != etagFromClient) { - CopyRespToWithInput(ref input, ref value, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: recordInfo.ETag); + CopyRespWithEtagData(ref value, ref output, hasEtagInRecord); return true; } @@ -349,15 +338,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } recordInfo.SetHasETag(); + // Increment the ETag - long newEtag = oldEtag + 1; + long newEtag = functionsState.etagState.etag + 1; - // Adjust value length if user shrinks it, how to get rid of spanbyte infront value.ShrinkSerializedLength(metadataSize + inputValue.Length + Constants.EtagSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - *(long*)value.ToPointer() = newEtag; + value.SetEtagInPayload(newEtag); + inputValue.ReadOnlySpan.CopyTo(value.AsSpan(Constants.EtagSize)); // write back array of the format [etag, nil] @@ -372,29 +362,25 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // If the user calls withetag then we need to either update an existing etag and set the value or set the value with an etag and increment it. bool inputHeaderHasEtag = input.header.CheckWithEtagFlag(); - int nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - int nextUpdateEtagOffset = etagIgnoredOffset; + int nextUpdateEtagOffset = functionsState.etagState.etagSkippedStart; // only when both are not false && false or true and true, do we need to readjust - if (inputHeaderHasEtag != shouldUpdateEtag) + if (inputHeaderHasEtag != hasEtagInRecord) { // in the common path the above condition is skipped if (inputHeaderHasEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; - shouldUpdateEtag = true; + hasEtagInRecord = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + this.functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; } else { - // nextUpdate will remove etag but currentyly there is an etag + // nextUpdate will remove etag but currently there is an etag nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - this.functionsState.etagOffsetForVarlen = 0; - recordInfo.ClearHasETag(); + this.functionsState.etagState.etagOffsetForVarlen = 0; } } @@ -410,7 +396,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); } // Adjust value length @@ -427,12 +413,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (inputHeaderHasEtag) { recordInfo.SetHasETag(); - *(long*)value.ToPointer() = oldEtag + 1; + value.SetEtagInPayload(functionsState.etagState.etag + 1); // withetag flag means we need to write etag back to the output buffer - CopyRespNumber(oldEtag + 1, ref output); + CopyRespNumber(functionsState.etagState.etag + 1, ref output); // early return since we already updated etag return true; } + else + { + recordInfo.ClearHasETag(); + } break; case RespCommand.SETKEEPTTLXX: @@ -442,29 +432,25 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // If withEtag is called we return the etag back to the user inputHeaderHasEtag = input.header.CheckWithEtagFlag(); - nextUpdateEtagIgnoredEnd = etagIgnoredEnd; - nextUpdateEtagOffset = etagIgnoredOffset; + nextUpdateEtagOffset = functionsState.etagState.etagSkippedStart; // only when both are not false && false or true and true, do we need to readjust - if (inputHeaderHasEtag != shouldUpdateEtag) + if (inputHeaderHasEtag != hasEtagInRecord) { // in the common path the above condition is skipped if (inputHeaderHasEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = value.LengthWithoutMetadata; - shouldUpdateEtag = true; + hasEtagInRecord = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; + functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; } else { // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; - this.functionsState.etagOffsetForVarlen = 0; - recordInfo.ClearHasETag(); + functionsState.etagState.etagOffsetForVarlen = 0; } } @@ -478,26 +464,30 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); } // Adjust value length rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); - value.ShrinkSerializedLength(setValue.Length + value.MetadataSize + etagIgnoredOffset); + value.ShrinkSerializedLength(setValue.Length + value.MetadataSize + functionsState.etagState.etagSkippedStart); // Copy input to value - setValue.ReadOnlySpan.CopyTo(value.AsSpan(etagIgnoredOffset)); + setValue.ReadOnlySpan.CopyTo(value.AsSpan(functionsState.etagState.etagSkippedStart)); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); if (inputHeaderHasEtag) { recordInfo.SetHasETag(); - *(long*)value.ToPointer() = oldEtag + 1; + value.SetEtagInPayload(functionsState.etagState.etag + 1); // withetag flag means we need to write etag back to the output buffer - CopyRespNumber(oldEtag + 1, ref output); + CopyRespNumber(functionsState.etagState.etag + 1, ref output); // early return since we already updated etag return true; } + else + { + recordInfo.ClearHasETag(); + } break; @@ -547,25 +537,25 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re return true; case RespCommand.INCR: - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: 1, functionsState.etagState.etagSkippedStart)) return false; break; case RespCommand.DECR: - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, functionsState.etagState.etagSkippedStart)) return false; break; case RespCommand.INCRBY: // Check if input contains a valid number var incrBy = input.arg1; - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: incrBy, functionsState.etagState.etagSkippedStart)) return false; break; case RespCommand.DECRBY: var decrBy = input.arg1; - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -decrBy, functionsState.etagState.etagSkippedStart)) return false; break; @@ -576,16 +566,16 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; return true; } - if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat, etagIgnoredOffset)) + if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat, functionsState.etagState.etagSkippedStart)) return false; break; case RespCommand.SETBIT: - var v = value.ToPointer() + etagIgnoredOffset; + var v = value.ToPointer() + functionsState.etagState.etagSkippedStart; var bOffset = input.parseState.GetLong(0); var bSetVal = (byte)(input.parseState.GetArgSliceByRef(1).ReadOnlySpan[0] - '0'); - if (!BitmapManager.IsLargeEnough(value.Length - etagIgnoredOffset, bOffset)) return false; + if (!BitmapManager.IsLargeEnough(functionsState.etagState.etagAccountedLength, bOffset)) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); value.UnmarkExtraMetadata(); @@ -600,9 +590,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re break; case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); - v = value.ToPointer() + etagIgnoredOffset; + v = value.ToPointer() + functionsState.etagState.etagSkippedStart; - if (!BitmapManager.IsLargeEnoughForType(bitFieldArgs, value.Length - etagIgnoredOffset)) + if (!BitmapManager.IsLargeEnoughForType(bitFieldArgs, value.Length - functionsState.etagState.etagSkippedStart)) return false; rmwInfo.ClearExtraValueLength(ref recordInfo, ref value, value.TotalSize); @@ -610,7 +600,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.ShrinkSerializedLength(value.Length + value.MetadataSize); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); - var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, v, value.Length - etagIgnoredOffset); + var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, v, value.Length - functionsState.etagState.etagSkippedStart); if (overflow) { @@ -660,23 +650,23 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var offset = input.parseState.GetInt(0); var newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - if (newValue.Length + offset > value.LengthWithoutMetadata - etagIgnoredOffset) + if (newValue.Length + offset > value.LengthWithoutMetadata - functionsState.etagState.etagSkippedStart) return false; - newValue.CopyTo(value.AsSpan(etagIgnoredOffset).Slice(offset)); + newValue.CopyTo(value.AsSpan(functionsState.etagState.etagSkippedStart).Slice(offset)); - CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref value, ref output, functionsState.etagState.etagSkippedStart); break; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); rmwInfo.Action = RMWAction.ExpireAndStop; return false; case RespCommand.GETEX: - CopyRespTo(ref value, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref value, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); if (input.arg1 > 0) { @@ -711,7 +701,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (appendSize == 0) { - CopyValueLengthToOutput(ref value, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref value, ref output, functionsState.etagState.etagSkippedStart); return true; } @@ -719,7 +709,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re default: if (cmd > RespCommandExtensions.LastValidCommand) { - if (recordInfo.ETag) + if (hasEtagInRecord) { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); return true; @@ -770,9 +760,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // increment the Etag transparently if in place update happened - if (shouldUpdateEtag) + if (hasEtagInRecord) { - *(long*)value.ToPointer() = oldEtag + 1; + value.SetEtagInPayload(this.functionsState.etagState.etag + 1); } return true; @@ -781,35 +771,19 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re /// public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; - this.functionsState.etagOffsetForVarlen = 0; - bool shouldUpdateEtag = rmwInfo.RecordInfo.ETag; - if (shouldUpdateEtag) - { - // used in varlen - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - oldEtag = *(long*)oldValue.ToPointer(); - // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; - } - switch (input.header.cmd) { case RespCommand.SETIFMATCH: - long existingEtag = oldEtag; long etagToCheckWith = input.parseState.GetLong(1); - if (existingEtag == etagToCheckWith) + if (functionsState.etagState.etag == etagToCheckWith) { return true; } else { - CopyRespToWithInput(ref input, ref oldValue, ref output, isFromPending: false, etagIgnoredOffset, etagIgnoredEnd, hasEtagInVal: rmwInfo.RecordInfo.ETag); + CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag); return false; } @@ -823,15 +797,14 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB return false; } - // since this block is only hit when this an update, the NX is violated and so we can return early from it without setting the value - + // since this case is only hit when this an update, the NX is violated and so we can return early from it without setting the value if (input.header.NotSetGetNorCheckWithEtag()) return false; if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); } else if (input.header.CheckWithEtagFlag()) { @@ -845,6 +818,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB // ExpireAndStop ensures that caller sees a NOTFOUND status if (oldValue.MetadataSize > 0 && input.header.CheckExpiry(oldValue.ExtraMetadata)) { + rmwInfo.RecordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndStop; return false; } @@ -854,7 +828,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) - .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), ref outp); + .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(functionsState.etagState.etagSkippedStart), ref outp); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -869,6 +843,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Expired data if (oldValue.MetadataSize > 0 && input.header.CheckExpiry(oldValue.ExtraMetadata)) { + recordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndResume; return false; } @@ -876,26 +851,12 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); RespCommand cmd = input.header.cmd; - bool shouldUpdateEtag = recordInfo.ETag; - - int etagIgnoredOffset = 0; - int etagIgnoredEnd = -1; - long oldEtag = Constants.BaseEtag; - this.functionsState.etagOffsetForVarlen = 0; - if (shouldUpdateEtag) - { - // used in varlen - etagIgnoredOffset = Constants.EtagSize; - etagIgnoredEnd = oldValue.LengthWithoutMetadata; - oldEtag = *(long*)oldValue.ToPointer(); - // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagOffsetForVarlen = Constants.EtagSize; - } + bool recordHasEtag = recordInfo.ETag; switch (cmd) { case RespCommand.SETIFMATCH: - shouldUpdateEtag = true; + recordHasEtag = true; // Copy input to value Span dest = newValue.AsSpan(Constants.EtagSize); ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -913,7 +874,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte newValue.ExtraMetadata = input.arg1; } - long newEtag = oldEtag + 1; + long newEtag = functionsState.etagState.etag + 1; recordInfo.SetHasETag(); @@ -926,27 +887,25 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.SET: case RespCommand.SETEXXX: - var nextUpdateEtagOffset = etagIgnoredOffset; - var nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + var nextUpdateEtagOffset = functionsState.etagState.etagSkippedStart; + var nextUpdateEtagAccountedLength = functionsState.etagState.etagAccountedLength; bool inputWithEtag = input.header.CheckWithEtagFlag(); // only when both are not false && false or true and true, do we need to readjust - if (inputWithEtag != shouldUpdateEtag) + if (inputWithEtag != recordHasEtag) { // in the common path the above condition is skipped if (inputWithEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = newValue.LengthWithoutMetadata; - shouldUpdateEtag = true; + recordHasEtag = true; recordInfo.SetHasETag(); } else { // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; recordInfo.ClearHasETag(); } } @@ -956,7 +915,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte { Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); } // Copy input to value @@ -971,34 +930,32 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (inputWithEtag) { - CopyRespNumber(oldEtag + 1, ref output); + CopyRespNumber(functionsState.etagState.etag + 1, ref output); } break; case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: - nextUpdateEtagOffset = etagIgnoredOffset; - nextUpdateEtagIgnoredEnd = etagIgnoredEnd; + nextUpdateEtagOffset = functionsState.etagState.etagSkippedStart; + nextUpdateEtagAccountedLength = functionsState.etagState.etagAccountedLength; inputWithEtag = input.header.CheckWithEtagFlag(); // only when both are not false && false or true and true, do we need to readjust - if (inputWithEtag != shouldUpdateEtag) + if (inputWithEtag != recordHasEtag) { // in the common path the above condition is skipped if (inputWithEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - nextUpdateEtagIgnoredEnd = newValue.LengthWithoutMetadata; - shouldUpdateEtag = true; + recordHasEtag = true; recordInfo.SetHasETag(); } else { // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; - nextUpdateEtagIgnoredEnd = -1; recordInfo.ClearHasETag(); } } @@ -1012,7 +969,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte { Debug.Assert(!input.header.CheckWithEtagFlag(), "SET GET CANNNOT BE CALLED WITH WITHETAG"); // Copy value to output for the GET part of the command. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); } // Copy input to value, retain metadata of oldValue @@ -1021,14 +978,14 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (inputWithEtag) { - CopyRespNumber(oldEtag + 1, ref output); + CopyRespNumber(functionsState.etagState.etag + 1, ref output); } break; case RespCommand.EXPIRE: case RespCommand.PEXPIRE: - shouldUpdateEtag = false; + recordHasEtag = false; var expiryExists = oldValue.MetadataSize > 0; @@ -1045,7 +1002,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.PEXPIREAT: case RespCommand.EXPIREAT: expiryExists = oldValue.MetadataSize > 0; - shouldUpdateEtag = false; + recordHasEtag = false; var expiryTimestamp = input.parseState.GetLong(0); expiryTicks = input.header.cmd == RespCommand.PEXPIREAT @@ -1057,7 +1014,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.PERSIST: - shouldUpdateEtag = false; + recordHasEtag = false; oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); if (oldValue.MetadataSize != 0) { @@ -1069,21 +1026,21 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.INCR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: 1, functionsState.etagState.etagSkippedStart); break; case RespCommand.DECR: - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -1, functionsState.etagState.etagSkippedStart); break; case RespCommand.INCRBY: var incrBy = input.arg1; - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrBy, functionsState.etagState.etagSkippedStart); break; case RespCommand.DECRBY: var decrBy = input.arg1; - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: -decrBy, functionsState.etagState.etagSkippedStart); break; case RespCommand.INCRBYFLOAT: @@ -1094,7 +1051,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue.CopyTo(ref newValue); break; } - TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat, etagIgnoredOffset); + TryCopyUpdateNumber(ref oldValue, ref newValue, ref output, input: incrByFloat, functionsState.etagState.etagSkippedStart); break; case RespCommand.SETBIT: @@ -1110,8 +1067,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.BITFIELD: var bitFieldArgs = GetBitFieldArguments(ref input); - Buffer.MemoryCopy(oldValue.ToPointer() + etagIgnoredOffset, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset, oldValue.Length - etagIgnoredOffset); - var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer() + etagIgnoredOffset, newValue.Length - etagIgnoredOffset); + Buffer.MemoryCopy(oldValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.Length - functionsState.etagState.etagSkippedStart, oldValue.Length - functionsState.etagState.etagSkippedStart); + var (bitfieldReturnValue, overflow) = BitmapManager.BitFieldExecute(bitFieldArgs, newValue.ToPointer() + functionsState.etagState.etagSkippedStart, newValue.Length - functionsState.etagState.etagSkippedStart); if (!overflow) CopyRespNumber(bitfieldReturnValue, ref output); @@ -1148,21 +1105,21 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte oldValue.CopyTo(ref newValue); newInputValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - newInputValue.CopyTo(newValue.AsSpan(etagIgnoredOffset).Slice(offset)); + newInputValue.CopyTo(newValue.AsSpan(functionsState.etagState.etagSkippedStart).Slice(offset)); - CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref newValue, ref output, functionsState.etagState.etagSkippedStart); break; case RespCommand.GETDEL: // Copy value to output for the GET part of the command. // Then, set ExpireAndStop action to delete the record. - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); rmwInfo.Action = RMWAction.ExpireAndStop; return false; case RespCommand.GETEX: - shouldUpdateEtag = false; - CopyRespTo(ref oldValue, ref output, etagIgnoredOffset, etagIgnoredEnd); + recordHasEtag = false; + CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); if (input.arg1 > 0) { @@ -1198,7 +1155,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Append the new value with the client input at the end of the old data appendValue.ReadOnlySpan.CopyTo(newValue.AsSpan().Slice(oldValue.LengthWithoutMetadata)); - CopyValueLengthToOutput(ref newValue, ref output, etagIgnoredOffset); + CopyValueLengthToOutput(ref newValue, ref output, functionsState.etagState.etagSkippedStart); break; default: @@ -1226,7 +1183,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functions - .CopyUpdater(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(etagIgnoredOffset), newValue.AsSpan(etagIgnoredOffset), ref outp, ref rmwInfo); + .CopyUpdater(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(functionsState.etagState.etagSkippedStart), newValue.AsSpan(functionsState.etagState.etagSkippedStart), ref outp, ref rmwInfo); output.Memory = outp.Memory; output.Length = outp.Length; return ret; @@ -1236,9 +1193,9 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - if (shouldUpdateEtag) + if (recordHasEtag) { - *(long*)newValue.ToPointer() = oldEtag + 1; + newValue.SetEtagInPayload(functionsState.etagState.etag + 1); } return true; diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 4527876451..08e0866f1d 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -20,29 +20,26 @@ public bool SingleReader( ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) { if (value.MetadataSize != 0 && CheckExpiry(ref value)) + { + readInfo.RecordInfo.ClearHasETag(); return false; + } var cmd = input.header.cmd; - var etagMultiplier = readInfo.RecordInfo.HasETagMultiplier; - if (cmd == RespCommand.GETIFNOTMATCH) { - long etagToMatchAgainst = input.parseState.GetLong(0); - // Any value without an etag is treated the same as a value with an etag - long existingEtag = etagMultiplier * *(long*)value.ToPointer(); - if (existingEtag == etagToMatchAgainst) - { - // write back array of the format [etag, nil] - var nilResp = CmdStrings.RESP_ERRNOTFOUND; - // *2\r\n: + + \r\n + - var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, writeDirect: true); + if (handleGetIfNotMatch(ref input, ref value, ref dst, ref readInfo)) return true; - } } else if (cmd > RespCommandExtensions.LastValidCommand) { + if (readInfo.RecordInfo.ETag) + { + CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref dst); + return true; + } + var valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) output = (dst.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)cmd) @@ -53,21 +50,23 @@ public bool SingleReader( return ret; } - int start = 0; - int end = -1; - // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - if (readInfo.RecordInfo.ETag && cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + if (readInfo.RecordInfo.ETag) { - start = Constants.EtagSize; - end = value.LengthWithoutMetadata; + functionsState.etagState.SetValsForRecordWithEtag(ref value); } + // Unless the command explicitly asks for the ETag in response, we do not write back the ETag + if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + { + CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag); + return true; + } if (cmd == RespCommand.NONE) - CopyRespTo(ref value, ref dst, start, end); + CopyRespTo(ref value, ref dst, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, readInfo.RecordInfo.ETag); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending); } return true; @@ -79,29 +78,26 @@ public bool ConcurrentReader( ref SpanByteAndMemory dst, ref ReadInfo readInfo, ref RecordInfo recordInfo) { if (value.MetadataSize != 0 && CheckExpiry(ref value)) + { + recordInfo.ClearHasETag(); return false; + } var cmd = input.header.cmd; - var etagMultiplier = readInfo.RecordInfo.HasETagMultiplier; - if (cmd == RespCommand.GETIFNOTMATCH) { - long etagToMatchAgainst = input.parseState.GetLong(0); - // Any value without an etag is treated the same as a value with an etag - long existingEtag = etagMultiplier * *(long*)value.ToPointer(); - if (existingEtag == etagToMatchAgainst) - { - // write back array of the format [etag, nil] - var nilResp = CmdStrings.RESP_ERRNOTFOUND; - // *2\r\n: + + \r\n + - var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, writeDirect: true); + if (handleGetIfNotMatch(ref input, ref value, ref dst, ref readInfo)) return true; - } } else if (cmd > RespCommandExtensions.LastValidCommand) { + if (readInfo.RecordInfo.ETag) + { + CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref dst); + return true; + } + var valueLength = value.LengthWithoutMetadata; (IMemoryOwner Memory, int Length) output = (dst.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)cmd) @@ -112,23 +108,47 @@ public bool ConcurrentReader( return ret; } - int start = 0; - int end = -1; + if (readInfo.RecordInfo.ETag) + { + functionsState.etagState.SetValsForRecordWithEtag(ref value); + } + // Unless the command explicitly asks for the ETag in response, we do not write back the ETag - if (recordInfo.ETag && cmd is not (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) + if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { - start = Constants.EtagSize; - end = value.LengthWithoutMetadata; + CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag); + return true; } + if (cmd == RespCommand.NONE) - CopyRespTo(ref value, ref dst, start, end); + CopyRespTo(ref value, ref dst, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); else { - CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending, start, end, readInfo.RecordInfo.ETag); + CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending); } return true; } + + private bool handleGetIfNotMatch(ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory dst, ref ReadInfo readInfo) + { + // Any value without an etag is treated the same as a value with an etag + long etagToMatchAgainst = input.parseState.GetLong(0); + + long existingEtag = readInfo.RecordInfo.ETag ? value.GetEtagInPayload() : Constants.BaseEtag; + + if (existingEtag == etagToMatchAgainst) + { + // write back array of the format [etag, nil] + var nilResp = CmdStrings.RESP_ERRNOTFOUND; + // *2\r\n: + + \r\n + + var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, writeDirect: true); + return true; + } + + return false; + } } } \ No newline at end of file diff --git a/libs/server/Storage/Functions/MainStore/UpsertMethods.cs b/libs/server/Storage/Functions/MainStore/UpsertMethods.cs index 15fb47ecb2..4bf44de3b3 100644 --- a/libs/server/Storage/Functions/MainStore/UpsertMethods.cs +++ b/libs/server/Storage/Functions/MainStore/UpsertMethods.cs @@ -13,6 +13,7 @@ namespace Garnet.server /// public bool SingleWriter(ref SpanByte key, ref RawStringInput input, ref SpanByte src, ref SpanByte dst, ref SpanByteAndMemory output, ref UpsertInfo upsertInfo, WriteReason reason, ref RecordInfo recordInfo) { + // Since upsert may be on existing key we need to wipe out the record info property recordInfo.ClearHasETag(); return SpanByteFunctions.DoSafeCopy(ref src, ref dst, ref upsertInfo, ref recordInfo, input.arg1); } @@ -28,6 +29,7 @@ public void PostSingleWriter(ref SpanByte key, ref RawStringInput input, ref Spa /// public bool ConcurrentWriter(ref SpanByte key, ref RawStringInput input, ref SpanByte src, ref SpanByte dst, ref SpanByteAndMemory output, ref UpsertInfo upsertInfo, ref RecordInfo recordInfo) { + // Since upsert may be on existing key we need to wipe out the record info property recordInfo.ClearHasETag(); if (ConcurrentWriterWorker(ref src, ref dst, ref input, ref upsertInfo, ref recordInfo)) { diff --git a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs index ed8379e1fd..3d836eaf12 100644 --- a/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs +++ b/libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs @@ -130,7 +130,7 @@ public int GetRMWInitialValueLength(ref RawStringInput input) return sizeof(int) + metadataSize + functions.GetInitialLength(ref input); } - return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + this.functionsState.etagOffsetForVarlen; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).ReadOnlySpan.Length + (input.arg1 == 0 ? 0 : sizeof(long)) + this.functionsState.etagState.etagOffsetForVarlen; } } @@ -141,45 +141,43 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) { var cmd = input.header.cmd; - int etagOffset = this.functionsState.etagOffsetForVarlen; - switch (cmd) { case RespCommand.INCR: case RespCommand.INCRBY: var incrByValue = input.header.cmd == RespCommand.INCRBY ? input.arg1 : 1; - var curr = NumUtils.BytesToLong(t.AsSpan(etagOffset)); + var curr = NumUtils.BytesToLong(t.AsSpan(functionsState.etagState.etagOffsetForVarlen)); var next = curr + incrByValue; var fNeg = false; var ndigits = NumUtils.NumDigitsInLong(next, ref fNeg); ndigits += fNeg ? 1 : 0; - return sizeof(int) + ndigits + t.MetadataSize + etagOffset; + return sizeof(int) + ndigits + t.MetadataSize + functionsState.etagState.etagOffsetForVarlen; case RespCommand.DECR: case RespCommand.DECRBY: var decrByValue = input.header.cmd == RespCommand.DECRBY ? input.arg1 : 1; - curr = NumUtils.BytesToLong(t.AsSpan(etagOffset)); + curr = NumUtils.BytesToLong(t.AsSpan(functionsState.etagState.etagOffsetForVarlen)); next = curr - decrByValue; fNeg = false; ndigits = NumUtils.NumDigitsInLong(next, ref fNeg); ndigits += fNeg ? 1 : 0; - return sizeof(int) + ndigits + t.MetadataSize + etagOffset; + return sizeof(int) + ndigits + t.MetadataSize + functionsState.etagState.etagOffsetForVarlen; case RespCommand.INCRBYFLOAT: // We don't need to TryGetDouble here because InPlaceUpdater will raise an error before we reach this point var incrByFloat = input.parseState.GetDouble(0); - NumUtils.TryBytesToDouble(t.AsSpan(etagOffset), out var currVal); + NumUtils.TryBytesToDouble(t.AsSpan(functionsState.etagState.etagOffsetForVarlen), out var currVal); var nextVal = currVal + incrByFloat; ndigits = NumUtils.NumOfCharInDouble(nextVal, out _, out _, out _); - return sizeof(int) + ndigits + t.MetadataSize + etagOffset; + return sizeof(int) + ndigits + t.MetadataSize + functionsState.etagState.etagOffsetForVarlen; case RespCommand.SETBIT: var bOffset = input.parseState.GetLong(0); return sizeof(int) + BitmapManager.NewBlockAllocLength(t.Length, bOffset); @@ -202,11 +200,11 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) case RespCommand.SETKEEPTTLXX: case RespCommand.SETKEEPTTL: var setValue = input.parseState.GetArgSliceByRef(0); - return sizeof(int) + t.MetadataSize + setValue.Length + etagOffset; + return sizeof(int) + t.MetadataSize + setValue.Length + functionsState.etagState.etagOffsetForVarlen; case RespCommand.SET: case RespCommand.SETEXXX: - return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + etagOffset; + return sizeof(int) + input.parseState.GetArgSliceByRef(0).Length + (input.arg1 == 0 ? 0 : sizeof(long)) + functionsState.etagState.etagOffsetForVarlen; case RespCommand.PERSIST: return sizeof(int) + t.LengthWithoutMetadata; case RespCommand.SETIFMATCH: @@ -223,8 +221,8 @@ public int GetRMWModifiedValueLength(ref SpanByte t, ref RawStringInput input) var offset = input.parseState.GetInt(0); newValue = input.parseState.GetArgSliceByRef(1).ReadOnlySpan; - if (newValue.Length + offset > t.LengthWithoutMetadata - etagOffset) - return sizeof(int) + newValue.Length + offset + t.MetadataSize + etagOffset; + if (newValue.Length + offset > t.LengthWithoutMetadata - functionsState.etagState.etagOffsetForVarlen) + return sizeof(int) + newValue.Length + offset + t.MetadataSize + functionsState.etagState.etagOffsetForVarlen; return sizeof(int) + t.Length; case RespCommand.GETDEL: diff --git a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs index 051d59630c..99197f2205 100644 --- a/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs +++ b/libs/storage/Tsavorite/cs/src/core/VarLen/SpanByte.cs @@ -498,6 +498,19 @@ public void CopyTo(byte* destination) } } + /// + /// Gets an Etag from the payload of the SpanByte, caller should make sure the SpanByte has an Etag for the record by checking RecordInfo + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetEtagInPayload() => *(long*)this.ToPointer(); + + /// + /// Gets an Etag from the payload of the SpanByte, caller should make sure the SpanByte has an Etag for the record by checking RecordInfo + /// + /// The Etag value to set + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetEtagInPayload(long etag) => *(long*)this.ToPointer() = etag; + /// public override string ToString() { diff --git a/test/Garnet.test/RespAofTests.cs b/test/Garnet.test/RespAofTests.cs index e71b5be4b2..90e8bb61e4 100644 --- a/test/Garnet.test/RespAofTests.cs +++ b/test/Garnet.test/RespAofTests.cs @@ -231,6 +231,8 @@ public void AofRMWStoreRecoverTest() db.StringSet("SeAofUpsertRecoverTestKey2", "SeAofUpsertRecoverTestValue2", expiry: TimeSpan.FromDays(1), when: When.NotExists); db.Execute("SET", "SeAofUpsertRecoverTestKey3", "SeAofUpsertRecoverTestValue3", "WITHETAG"); db.Execute("SETIFMATCH", "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", "1"); + db.Execute("SET", "SeAofUpsertRecoverTestKey4", "2"); + var res = db.Execute("INCR", "SeAofUpsertRecoverTestKey4"); } server.Store.CommitAOF(true); @@ -246,6 +248,8 @@ public void AofRMWStoreRecoverTest() recoveredValue = db.StringGet("SeAofUpsertRecoverTestKey2"); ClassicAssert.AreEqual("SeAofUpsertRecoverTestValue2", recoveredValue.ToString()); ExpectedEtagTest(db, "SeAofUpsertRecoverTestKey3", "UpdatedSeAofUpsertRecoverTestValue3", 2); + recoveredValue = db.StringGet("SeAofUpsertRecoverTestKey4"); + ClassicAssert.AreEqual("3", recoveredValue.ToString()); } } diff --git a/test/Garnet.test/RespEtagTests.cs b/test/Garnet.test/RespEtagTests.cs index 109dc865b6..3bd15c489e 100644 --- a/test/Garnet.test/RespEtagTests.cs +++ b/test/Garnet.test/RespEtagTests.cs @@ -1351,7 +1351,7 @@ public void KeyExpireStringTestForEtagSetData(string command) var db = redis.GetDatabase(0); var key = "keyA"; - db.Execute("SET", [key, key]); + db.Execute("SET", [key, key, "WITHETAG"]); var value = db.StringGet(key); ClassicAssert.AreEqual(key, (string)value); From 9de234d50bd7aeb48615434aab40947a76b88d2d Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Wed, 8 Jan 2025 21:47:11 -0800 Subject: [PATCH 76/87] Badrish PR feedback --- libs/common/RespWriteUtils.cs | 58 +++++++++++++++++++ .../Functions/MainStore/PrivateMethods.cs | 58 ------------------- .../Storage/Functions/MainStore/RMWMethods.cs | 8 +-- .../Functions/MainStore/ReadMethods.cs | 6 +- .../cs/src/core/Index/Common/RecordInfo.cs | 5 -- 5 files changed, 65 insertions(+), 70 deletions(-) diff --git a/libs/common/RespWriteUtils.cs b/libs/common/RespWriteUtils.cs index 557fc09fba..eded783e37 100644 --- a/libs/common/RespWriteUtils.cs +++ b/libs/common/RespWriteUtils.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using Tsavorite.core; namespace Garnet.common { @@ -697,6 +698,63 @@ public static bool WriteArrayWithNullElements(int len, ref byte* curr, byte* end return true; } + public static void CopyRespWithEtagData(ref SpanByte value, ref SpanByteAndMemory dst, bool hasEtagInVal, int etagSkippedStart, MemoryPool memoryPool) + { + int valueLength = value.LengthWithoutMetadata; + // always writing an array of size 2 => *2\r\n + int desiredLength = 4; + ReadOnlySpan etagTruncatedVal; + // get etag to write, default etag 0 for when no etag + long etag = hasEtagInVal ? value.GetEtagInPayload() : 0; // BaseEtag + // remove the length of the ETAG + var etagAccountedValueLength = valueLength - etagSkippedStart; + if (hasEtagInVal) + { + etagAccountedValueLength = valueLength - sizeof(long); // EtagSize + } + + // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below + etagTruncatedVal = value.AsReadOnlySpan(etagSkippedStart); + // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n + desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; + + WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst, memoryPool); + } + + public static void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst, MemoryPool memoryPool, bool writeDirect = false) + { + if (desiredLength <= dst.Length) + { + dst.Length = desiredLength; + byte* curr = dst.SpanByte.ToPointer(); + byte* end = curr + dst.SpanByte.Length; + RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); + return; + } + + dst.ConvertToHeap(); + dst.Length = desiredLength; + dst.Memory = memoryPool.Rent(desiredLength); + fixed (byte* ptr = dst.Memory.Memory.Span) + { + byte* curr = ptr; + byte* end = ptr + desiredLength; + RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); + } + } + + public static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end, bool writeDirect) + { + // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element + RespWriteUtils.WriteArrayLength(2, ref curr, end); + RespWriteUtils.WriteInteger(etag, ref curr, end); + + if (writeDirect) + RespWriteUtils.WriteDirect(value, ref curr, end); + else + RespWriteUtils.WriteBulkString(value, ref curr, end); + } + /// /// Write newline (\r\n) to /// diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 02ef1f0878..3ce6afc4d4 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -36,7 +36,6 @@ static void CopyTo(ref SpanByte src, ref SpanByteAndMemory dst, MemoryPool void CopyRespTo(ref SpanByte src, ref SpanByteAndMemory dst, int start = 0, int end = -1) { - // src length of the value indicating no end is supplied defaults to lengthWithoutMetadata, else it chooses the bigger of 0 or (end - start) int srcLength = end == -1 ? src.LengthWithoutMetadata : ((start < end) ? (end - start) : 0); if (srcLength == 0) { @@ -259,63 +258,6 @@ void CopyRespToWithInput(ref RawStringInput input, ref SpanByte value, ref SpanB } } - void CopyRespWithEtagData(ref SpanByte value, ref SpanByteAndMemory dst, bool hasEtagInVal) - { - int valueLength = value.LengthWithoutMetadata; - // always writing an array of size 2 => *2\r\n - int desiredLength = 4; - ReadOnlySpan etagTruncatedVal; - // get etag to write, default etag 0 for when no etag - long etag = hasEtagInVal ? value.GetEtagInPayload() : Constants.BaseEtag; - // remove the length of the ETAG - var etagAccountedValueLength = valueLength - functionsState.etagState.etagSkippedStart; - if (hasEtagInVal) - { - etagAccountedValueLength = valueLength - Constants.EtagSize; - } - - // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below - etagTruncatedVal = value.AsReadOnlySpan(functionsState.etagState.etagSkippedStart); - // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n - desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; - - WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst); - } - - void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst, bool writeDirect = false) - { - if (desiredLength <= dst.Length) - { - dst.Length = desiredLength; - byte* curr = dst.SpanByte.ToPointer(); - byte* end = curr + dst.SpanByte.Length; - RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); - return; - } - - dst.ConvertToHeap(); - dst.Length = desiredLength; - dst.Memory = functionsState.memoryPool.Rent(desiredLength); - fixed (byte* ptr = dst.Memory.Memory.Span) - { - byte* curr = ptr; - byte* end = ptr + desiredLength; - RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); - } - } - - static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end, bool writeDirect) - { - // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element - RespWriteUtils.WriteArrayLength(2, ref curr, end); - RespWriteUtils.WriteInteger(etag, ref curr, end); - - if (writeDirect) - RespWriteUtils.WriteDirect(value, ref curr, end); - else - RespWriteUtils.WriteBulkString(value, ref curr, end); - } - bool EvaluateExpireInPlace(ExpireOption optionType, bool expiryExists, long newExpiry, ref SpanByte value, ref SpanByteAndMemory output) { ObjectOutputHeader* o = (ObjectOutputHeader*)output.SpanByte.ToPointer(); diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 8944db1dd3..2c8e064fa7 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -319,7 +319,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re long etagFromClient = input.parseState.GetLong(1); if (functionsState.etagState.etag != etagFromClient) { - CopyRespWithEtagData(ref value, ref output, hasEtagInRecord); + RespWriteUtils.CopyRespWithEtagData(ref value, ref output, hasEtagInRecord, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); return true; } @@ -354,7 +354,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, writeDirect: true); + RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); // early return since we already updated the ETag return true; case RespCommand.SET: @@ -783,7 +783,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB } else { - CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag); + RespWriteUtils.CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); return false; } @@ -883,7 +883,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, writeDirect: true); + RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool,writeDirect: true); break; case RespCommand.SET: case RespCommand.SETEXXX: diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 08e0866f1d..96217df6d2 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -58,7 +58,7 @@ public bool SingleReader( // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { - CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag); + RespWriteUtils.CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); return true; } @@ -116,7 +116,7 @@ public bool ConcurrentReader( // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { - CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag); + RespWriteUtils.CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); return true; } @@ -144,7 +144,7 @@ private bool handleGetIfNotMatch(ref RawStringInput input, ref SpanByte value, r var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); - WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, writeDirect: true); + RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, functionsState.memoryPool, writeDirect: true); return true; } diff --git a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs index ade83cc984..5d82c473f5 100644 --- a/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs +++ b/libs/storage/Tsavorite/cs/src/core/Index/Common/RecordInfo.cs @@ -284,11 +284,6 @@ public bool ETag public void SetHasETag() => word |= kETagBitMask; public void ClearHasETag() => word &= ~kETagBitMask; - /// - /// When ETag is set this returns 1 else 0. Used for branchless programming - /// - public int HasETagMultiplier => (int)((word & kETagBitMask) >> kEtagBitOffset); - public override readonly string ToString() { var paRC = IsReadCache(PreviousAddress) ? "(rc)" : string.Empty; From 1ede8d95bebfeda4c918ddac0c7dc52e54a0cc9c Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 13:31:05 -0800 Subject: [PATCH 77/87] format --- libs/server/Storage/Functions/EtagState.cs | 2 +- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Storage/Functions/EtagState.cs b/libs/server/Storage/Functions/EtagState.cs index f9f0694be6..3771c2ba75 100644 --- a/libs/server/Storage/Functions/EtagState.cs +++ b/libs/server/Storage/Functions/EtagState.cs @@ -24,7 +24,7 @@ public class EtagState /// Resp response methods depend on the value for end being -1 or length of the payload. This field lets you work with providing the end opaquely. /// public int etagAccountedLength { get; private set; } = -1; - + /// /// Field provides access to getting an Etag from a record, hiding whether it is actually present or not. /// diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 2c8e064fa7..dabd91be6b 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -883,7 +883,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); - RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool,writeDirect: true); + RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); break; case RespCommand.SET: case RespCommand.SETEXXX: From a98ef06be43c07db2cefc72966973b5d538d1f17 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 18:37:33 -0800 Subject: [PATCH 78/87] pr feedback --- libs/common/RespWriteUtils.cs | 47 +----- libs/server/ACL/ACLParser.cs | 2 +- libs/server/ACL/CommandPermissionSet.cs | 4 +- libs/server/AOF/AofProcessor.cs | 1 - libs/server/API/GarnetApi.cs | 4 +- libs/server/Resp/BasicCommands.cs | 6 +- libs/server/Resp/Parser/RespCommand.cs | 4 +- libs/server/Resp/RespServerSession.cs | 8 +- .../Resp/RespServerSessionSlotVerify.cs | 2 +- .../Storage/Functions/FunctionsState.cs | 3 +- .../Functions/MainStore/PrivateMethods.cs | 45 ++++++ .../Storage/Functions/MainStore/RMWMethods.cs | 137 ++++++++++++++---- .../Functions/MainStore/ReadMethods.cs | 18 ++- libs/server/Transaction/TxnRespCommands.cs | 2 +- test/Garnet.test/Resp/ACL/RespCommandTests.cs | 2 +- website/docs/commands/garnet-specific.md | 4 +- 16 files changed, 189 insertions(+), 100 deletions(-) diff --git a/libs/common/RespWriteUtils.cs b/libs/common/RespWriteUtils.cs index eded783e37..81159c5f42 100644 --- a/libs/common/RespWriteUtils.cs +++ b/libs/common/RespWriteUtils.cs @@ -698,52 +698,7 @@ public static bool WriteArrayWithNullElements(int len, ref byte* curr, byte* end return true; } - public static void CopyRespWithEtagData(ref SpanByte value, ref SpanByteAndMemory dst, bool hasEtagInVal, int etagSkippedStart, MemoryPool memoryPool) - { - int valueLength = value.LengthWithoutMetadata; - // always writing an array of size 2 => *2\r\n - int desiredLength = 4; - ReadOnlySpan etagTruncatedVal; - // get etag to write, default etag 0 for when no etag - long etag = hasEtagInVal ? value.GetEtagInPayload() : 0; // BaseEtag - // remove the length of the ETAG - var etagAccountedValueLength = valueLength - etagSkippedStart; - if (hasEtagInVal) - { - etagAccountedValueLength = valueLength - sizeof(long); // EtagSize - } - - // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below - etagTruncatedVal = value.AsReadOnlySpan(etagSkippedStart); - // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n - desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; - - WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst, memoryPool); - } - - public static void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst, MemoryPool memoryPool, bool writeDirect = false) - { - if (desiredLength <= dst.Length) - { - dst.Length = desiredLength; - byte* curr = dst.SpanByte.ToPointer(); - byte* end = curr + dst.SpanByte.Length; - RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); - return; - } - - dst.ConvertToHeap(); - dst.Length = desiredLength; - dst.Memory = memoryPool.Rent(desiredLength); - fixed (byte* ptr = dst.Memory.Memory.Span) - { - byte* curr = ptr; - byte* end = ptr + desiredLength; - RespWriteEtagValArray(etag, ref value, ref curr, end, writeDirect); - } - } - - public static void RespWriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end, bool writeDirect) + public static void WriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end, bool writeDirect) { // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element RespWriteUtils.WriteArrayLength(2, ref curr, end); diff --git a/libs/server/ACL/ACLParser.cs b/libs/server/ACL/ACLParser.cs index fcd6183c8c..f033f3042a 100644 --- a/libs/server/ACL/ACLParser.cs +++ b/libs/server/ACL/ACLParser.cs @@ -296,7 +296,7 @@ static bool IsValidParse(RespCommand command, ReadOnlySpan fromStr) // Some commands aren't really commands, so ACLs shouldn't accept their names static bool IsInvalidCommandToAcl(RespCommand command) - => command == RespCommand.INVALID || command == RespCommand.NONE || command.Normalize() != command; + => command == RespCommand.INVALID || command == RespCommand.NONE || command.NormalizeForACLs() != command; } /// diff --git a/libs/server/ACL/CommandPermissionSet.cs b/libs/server/ACL/CommandPermissionSet.cs index 780db48c48..fc9f0cb694 100644 --- a/libs/server/ACL/CommandPermissionSet.cs +++ b/libs/server/ACL/CommandPermissionSet.cs @@ -91,7 +91,7 @@ public CommandPermissionSet Copy() /// public void AddCommand(RespCommand command) { - Debug.Assert(command.Normalize() == command, "Cannot control access to this command, it's an implementation detail"); + Debug.Assert(command.NormalizeForACLs() == command, "Cannot control access to this command, it's an implementation detail"); int index = (int)command; int ulongIndex = index / 64; @@ -118,7 +118,7 @@ public void AddCommand(RespCommand command) /// public void RemoveCommand(RespCommand command) { - Debug.Assert(command.Normalize() == command, "Cannot control access to this command, it's an implementation detail"); + Debug.Assert(command.NormalizeForACLs() == command, "Cannot control access to this command, it's an implementation detail"); // Can't remove access to these commands if (command.IsNoAuth()) diff --git a/libs/server/AOF/AofProcessor.cs b/libs/server/AOF/AofProcessor.cs index e73d3cb4bb..94bf35913c 100644 --- a/libs/server/AOF/AofProcessor.cs +++ b/libs/server/AOF/AofProcessor.cs @@ -134,7 +134,6 @@ private unsafe void RecoverReplay(long untilAddress) { count++; ProcessAofRecord(entry, length); - this.respServerSession.storageSession.functionsState.etagState.ResetToDefaultVals(); if (count % 100_000 == 0) logger?.LogInformation("Completed AOF replay of {count} records, until AOF address {nextAofAddress}", count, nextAofAddress); } diff --git a/libs/server/API/GarnetApi.cs b/libs/server/API/GarnetApi.cs index 7f2218abaf..23246de16c 100644 --- a/libs/server/API/GarnetApi.cs +++ b/libs/server/API/GarnetApi.cs @@ -180,11 +180,11 @@ public GarnetStatus APPEND(ArgSlice key, ArgSlice value, ref ArgSlice output) #region RENAME /// - public GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, bool withEtag, StoreType storeType = StoreType.All) + public GarnetStatus RENAME(ArgSlice oldKey, ArgSlice newKey, bool withEtag = false, StoreType storeType = StoreType.All) => storageSession.RENAME(oldKey, newKey, storeType, withEtag); /// - public GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, bool withEtag, StoreType storeType = StoreType.All) + public GarnetStatus RENAMENX(ArgSlice oldKey, ArgSlice newKey, out int result, bool withEtag = false, StoreType storeType = StoreType.All) => storageSession.RENAMENX(oldKey, newKey, storeType, out result, withEtag); #endregion diff --git a/libs/server/Resp/BasicCommands.cs b/libs/server/Resp/BasicCommands.cs index c7dccbf276..e86bdf8489 100644 --- a/libs/server/Resp/BasicCommands.cs +++ b/libs/server/Resp/BasicCommands.cs @@ -442,7 +442,7 @@ private bool NetworkSETNX(bool highPrecision, ref TGarnetApi storage enum EtagOption : byte { None, - WITHETAG, + WithETag, } enum ExpirationOption : byte @@ -591,7 +591,7 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) break; } - etagOption = EtagOption.WITHETAG; + etagOption = EtagOption.WithETag; } else { @@ -616,7 +616,7 @@ private bool NetworkSETEXNX(ref TGarnetApi storageApi) return true; } - bool withEtag = etagOption == EtagOption.WITHETAG; + bool withEtag = etagOption == EtagOption.WithETag; bool isHighPrecision = expOption == ExpirationOption.PX; diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index cdc38c9e9d..2e4c53d7a4 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -438,7 +438,7 @@ public static bool IsAofIndependent(this RespCommand cmd) /// for ACL'ing purposes and reading command info purposes /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static RespCommand Normalize(this RespCommand cmd) + public static RespCommand NormalizeForACLs(this RespCommand cmd) { return cmd switch @@ -453,7 +453,7 @@ public static RespCommand Normalize(this RespCommand cmd) } /// - /// Reverses , producing all the equivalent s which are covered by . + /// Reverses , producing all the equivalent s which are covered by . /// public static ReadOnlySpan ExpandForACLs(this RespCommand cmd) { diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index ff502a1e32..ea613d8500 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -408,7 +408,6 @@ private void ProcessMessages() while (bytesRead - readHead >= 4) { - storageSession.functionsState.etagState.ResetToDefaultVals(); // First, parse the command, making sure we have the entire command available // We use endReadHead to track the end of the current command // On success, readHead is left at the start of the command payload for legacy operators @@ -518,14 +517,11 @@ private bool ProcessBasicCommands(RespCommand cmd, ref TGarnetApi st { RespCommand.GET => NetworkGET(ref storageApi), RespCommand.GETEX => NetworkGETEX(ref storageApi), - RespCommand.GETWITHETAG => NetworkGETWITHETAG(ref storageApi), - RespCommand.GETIFNOTMATCH => NetworkGETIFNOTMATCH(ref storageApi), RespCommand.SET => NetworkSET(ref storageApi), RespCommand.SETEX => NetworkSETEX(false, ref storageApi), RespCommand.SETNX => NetworkSETNX(false, ref storageApi), RespCommand.PSETEX => NetworkSETEX(true, ref storageApi), RespCommand.SETEXNX => NetworkSETEXNX(ref storageApi), - RespCommand.SETIFMATCH => NetworkSETIFMATCH(ref storageApi), RespCommand.DEL => NetworkDEL(ref storageApi), RespCommand.RENAME => NetworkRENAME(ref storageApi), RespCommand.RENAMENX => NetworkRENAMENX(ref storageApi), @@ -566,6 +562,10 @@ private bool ProcessBasicCommands(RespCommand cmd, ref TGarnetApi st RespCommand.READWRITE => NetworkREADWRITE(), RespCommand.EXPIREAT => NetworkEXPIREAT(RespCommand.EXPIREAT, ref storageApi), RespCommand.PEXPIREAT => NetworkEXPIREAT(RespCommand.PEXPIREAT, ref storageApi), + // Etag related commands + RespCommand.GETWITHETAG => NetworkGETWITHETAG(ref storageApi), + RespCommand.GETIFNOTMATCH => NetworkGETIFNOTMATCH(ref storageApi), + RespCommand.SETIFMATCH => NetworkSETIFMATCH(ref storageApi), _ => ProcessArrayCommands(cmd, ref storageApi) }; diff --git a/libs/server/Resp/RespServerSessionSlotVerify.cs b/libs/server/Resp/RespServerSessionSlotVerify.cs index 2e7c450c79..323fb50be0 100644 --- a/libs/server/Resp/RespServerSessionSlotVerify.cs +++ b/libs/server/Resp/RespServerSessionSlotVerify.cs @@ -30,7 +30,7 @@ bool CanServeSlot(RespCommand cmd) if (!cmd.IsDataCommand()) return true; - cmd = cmd.Normalize(); + cmd = cmd.NormalizeForACLs(); if (!RespCommandsInfo.TryFastGetRespCommandInfo(cmd, out var commandInfo)) // This only happens if we failed to parse the json file return false; diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index 6b5f56e437..ca2eefddce 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System.Buffers; -using System.Net.Http; using Tsavorite.core; namespace Garnet.server @@ -19,7 +18,7 @@ internal sealed class FunctionsState public readonly MemoryPool memoryPool; public readonly CacheSizeTracker objectStoreSizeTracker; public readonly GarnetObjectSerializer garnetObjectSerializer; - public EtagState etagState; + public readonly EtagState etagState; public bool StoredProcMode; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 3ce6afc4d4..5c70023675 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -671,6 +671,51 @@ static void CopyValueLengthToOutput(ref SpanByte value, ref SpanByteAndMemory ou output.SpanByte.Length = numDigits; } + static void CopyRespWithEtagData(ref SpanByte value, ref SpanByteAndMemory dst, bool hasEtagInVal, int etagSkippedStart, MemoryPool memoryPool) + { + int valueLength = value.LengthWithoutMetadata; + // always writing an array of size 2 => *2\r\n + int desiredLength = 4; + ReadOnlySpan etagTruncatedVal; + // get etag to write, default etag 0 for when no etag + long etag = hasEtagInVal ? value.GetEtagInPayload() : 0; // BaseEtag + // remove the length of the ETAG + var etagAccountedValueLength = valueLength - etagSkippedStart; + if (hasEtagInVal) + { + etagAccountedValueLength = valueLength - sizeof(long); // EtagSize + } + + // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below + etagTruncatedVal = value.AsReadOnlySpan(etagSkippedStart); + // *2\r\n :(etag digits)\r\n $(val Len digits)\r\n (value len)\r\n + desiredLength += 1 + NumUtils.NumDigitsInLong(etag) + 2 + 1 + NumUtils.NumDigits(etagAccountedValueLength) + 2 + etagAccountedValueLength + 2; + + WriteValAndEtagToDst(desiredLength, ref etagTruncatedVal, etag, ref dst, memoryPool); + } + + static void WriteValAndEtagToDst(int desiredLength, ref ReadOnlySpan value, long etag, ref SpanByteAndMemory dst, MemoryPool memoryPool, bool writeDirect = false) + { + if (desiredLength <= dst.Length) + { + dst.Length = desiredLength; + byte* curr = dst.SpanByte.ToPointer(); + byte* end = curr + dst.SpanByte.Length; + RespWriteUtils.WriteEtagValArray(etag, ref value, ref curr, end, writeDirect); + return; + } + + dst.ConvertToHeap(); + dst.Length = desiredLength; + dst.Memory = memoryPool.Rent(desiredLength); + fixed (byte* ptr = dst.Memory.Memory.Span) + { + byte* curr = ptr; + byte* end = ptr + desiredLength; + RespWriteUtils.WriteEtagValArray(etag, ref value, ref curr, end, writeDirect); + } + } + /// /// Logging upsert from /// a. ConcurrentWriter diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index dabd91be6b..1f79e8c5d4 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -254,6 +254,10 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB /// public void PostInitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte value, ref SpanByteAndMemory output, ref RMWInfo rmwInfo) { + // reset etag state set at need initial update + if (input.header.cmd is (RespCommand.SET or RespCommand.SETEXNX or RespCommand.SETKEEPTTL)) + functionsState.etagState.ResetToDefaultVals(); + functionsState.watchVersionMap.IncrementVersion(rmwInfo.KeyHash); if (functionsState.appendOnlyFile != null) { @@ -288,9 +292,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } RespCommand cmd = input.header.cmd; - - bool hasEtagInRecord = recordInfo.ETag; - if (hasEtagInRecord) + bool hadRecordPreMutation = recordInfo.ETag; + bool shouldUpdateEtag = hadRecordPreMutation; + if (shouldUpdateEtag) { functionsState.etagState.SetValsForRecordWithEtag(ref value); } @@ -298,28 +302,29 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re switch (cmd) { case RespCommand.SETEXNX: - if (input.header.NotSetGetNorCheckWithEtag()) - return true; - if (input.header.CheckSetGetFlag()) { // Copy value to output for the GET part of the command. CopyRespTo(ref value, ref output); } - else + else if (input.header.CheckWithEtagFlag()) { // when called withetag all output needs to be placed on the buffer // EXX when unsuccesful will write back NIL CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } + // reset etag state after done using + functionsState.etagState.ResetToDefaultVals(); // Nothing is set because being in this block means NX was already violated return true; case RespCommand.SETIFMATCH: long etagFromClient = input.parseState.GetLong(1); if (functionsState.etagState.etag != etagFromClient) { - RespWriteUtils.CopyRespWithEtagData(ref value, ref output, hasEtagInRecord, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + CopyRespWithEtagData(ref value, ref output, shouldUpdateEtag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + // reset etag state after done using + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -354,7 +359,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); - RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); + // reset etag state after done using + functionsState.etagState.ResetToDefaultVals(); // early return since we already updated the ETag return true; case RespCommand.SET: @@ -365,19 +372,20 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re int nextUpdateEtagOffset = functionsState.etagState.etagSkippedStart; // only when both are not false && false or true and true, do we need to readjust - if (inputHeaderHasEtag != hasEtagInRecord) + if (inputHeaderHasEtag != shouldUpdateEtag) { // in the common path the above condition is skipped if (inputHeaderHasEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - hasEtagInRecord = true; + shouldUpdateEtag = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation this.functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; } else { + shouldUpdateEtag = false; // nextUpdate will remove etag but currently there is an etag nextUpdateEtagOffset = 0; this.functionsState.etagState.etagOffsetForVarlen = 0; @@ -416,6 +424,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.SetEtagInPayload(functionsState.etagState.etag + 1); // withetag flag means we need to write etag back to the output buffer CopyRespNumber(functionsState.etagState.etag + 1, ref output); + // reset etag state after done using + functionsState.etagState.ResetToDefaultVals(); // early return since we already updated etag return true; } @@ -435,19 +445,20 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re nextUpdateEtagOffset = functionsState.etagState.etagSkippedStart; // only when both are not false && false or true and true, do we need to readjust - if (inputHeaderHasEtag != hasEtagInRecord) + if (inputHeaderHasEtag != shouldUpdateEtag) { // in the common path the above condition is skipped if (inputHeaderHasEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - hasEtagInRecord = true; + shouldUpdateEtag = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; } else { + shouldUpdateEtag = true; // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; functionsState.etagState.etagOffsetForVarlen = 0; @@ -481,6 +492,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.SetEtagInPayload(functionsState.etagState.etag + 1); // withetag flag means we need to write etag back to the output buffer CopyRespNumber(functionsState.etagState.etag + 1, ref output); + // reset etag state after done using + functionsState.etagState.ResetToDefaultVals(); // early return since we already updated etag return true; } @@ -502,6 +515,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var expiryTicks = DateTimeOffset.UtcNow.Ticks + tsExpiry.Ticks; var expireOption = (ExpireOption)input.arg1; + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); + if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; @@ -517,6 +533,9 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re : ConvertUtils.UnixTimestampInSecondsToTicks(expiryTimestamp); expireOption = (ExpireOption)input.arg1; + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); + if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; @@ -534,6 +553,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re output.SpanByte.AsSpan()[0] = 1; } // does not update etag + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; case RespCommand.INCR: @@ -543,7 +564,10 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re case RespCommand.DECR: if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, input: -1, functionsState.etagState.etagSkippedStart)) + { + return false; + } break; case RespCommand.INCRBY: @@ -564,6 +588,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!input.parseState.TryGetDouble(0, out var incrByFloat)) { output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; } if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat, functionsState.etagState.etagSkippedStart)) @@ -605,6 +631,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (overflow) { CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); // etag not updated return true; } @@ -618,6 +646,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!HyperLogLog.DefaultHLL.IsValidHYLL(v, value.Length)) { *output.SpanByte.ToPointer() = (byte)0xFF; + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -638,6 +668,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!HyperLogLog.DefaultHLL.IsValidHYLL(dstHLL, value.Length)) { + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); //InvalidType *(long*)output.SpanByte.ToPointer() = -1; return true; @@ -689,10 +721,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.ShrinkSerializedLength(value.Length - value.MetadataSize); value.UnmarkExtraMetadata(); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; } } + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; case RespCommand.APPEND: @@ -702,6 +738,8 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (appendSize == 0) { CopyValueLengthToOutput(ref value, ref output, functionsState.etagState.etagSkippedStart); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -709,9 +747,11 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re default: if (cmd > RespCommandExtensions.LastValidCommand) { - if (hasEtagInRecord) + if (shouldUpdateEtag) { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -760,11 +800,17 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // increment the Etag transparently if in place update happened - if (hasEtagInRecord) + if (shouldUpdateEtag) { value.SetEtagInPayload(this.functionsState.etagState.etag + 1); } + if (hadRecordPreMutation) + { + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); + } + return true; } @@ -783,7 +829,9 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB } else { - RespWriteUtils.CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return false; } @@ -794,12 +842,12 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { rmwInfo.Action = RMWAction.ExpireAndResume; rmwInfo.RecordInfo.ClearHasETag(); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return false; } // since this case is only hit when this an update, the NX is violated and so we can return early from it without setting the value - if (input.header.NotSetGetNorCheckWithEtag()) - return false; if (input.header.CheckSetGetFlag()) { @@ -812,6 +860,8 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); } + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return false; case RespCommand.SETEXXX: // Expired data, return false immediately so we do not set, since it does not exist @@ -820,12 +870,21 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { rmwInfo.RecordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndStop; + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return false; } return true; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) { + if (rmwInfo.RecordInfo.ETag) + { + CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); + return false; + } (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(functionsState.etagState.etagSkippedStart), ref outp); @@ -845,18 +904,27 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte { recordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndResume; + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return false; } rmwInfo.ClearExtraValueLength(ref recordInfo, ref newValue, newValue.TotalSize); RespCommand cmd = input.header.cmd; - bool recordHasEtag = recordInfo.ETag; + + bool recordHadEtagPreMutation = recordInfo.ETag; + bool shouldUpdateEtag = recordHadEtagPreMutation; + if (shouldUpdateEtag) + { + // during checkpointing we might skip the inplace calls and go directly to copy update so we need to initialize here if needed + functionsState.etagState.SetValsForRecordWithEtag(ref oldValue); + } switch (cmd) { case RespCommand.SETIFMATCH: - recordHasEtag = true; + shouldUpdateEtag = true; // Copy input to value Span dest = newValue.AsSpan(Constants.EtagSize); ReadOnlySpan src = input.parseState.GetArgSliceByRef(0).ReadOnlySpan; @@ -883,7 +951,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); - RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); break; case RespCommand.SET: case RespCommand.SETEXXX: @@ -892,14 +960,14 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte bool inputWithEtag = input.header.CheckWithEtagFlag(); // only when both are not false && false or true and true, do we need to readjust - if (inputWithEtag != recordHasEtag) + if (inputWithEtag != shouldUpdateEtag) { // in the common path the above condition is skipped if (inputWithEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - recordHasEtag = true; + shouldUpdateEtag = true; recordInfo.SetHasETag(); } else @@ -942,18 +1010,19 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte inputWithEtag = input.header.CheckWithEtagFlag(); // only when both are not false && false or true and true, do we need to readjust - if (inputWithEtag != recordHasEtag) + if (inputWithEtag != shouldUpdateEtag) { // in the common path the above condition is skipped if (inputWithEtag) { // nextUpdate will add etag but currently there is no etag nextUpdateEtagOffset = Constants.EtagSize; - recordHasEtag = true; + shouldUpdateEtag = true; recordInfo.SetHasETag(); } else { + shouldUpdateEtag = false; // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; recordInfo.ClearHasETag(); @@ -985,7 +1054,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.EXPIRE: case RespCommand.PEXPIRE: - recordHasEtag = false; + shouldUpdateEtag = false; var expiryExists = oldValue.MetadataSize > 0; @@ -1002,7 +1071,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte case RespCommand.PEXPIREAT: case RespCommand.EXPIREAT: expiryExists = oldValue.MetadataSize > 0; - recordHasEtag = false; + shouldUpdateEtag = false; var expiryTimestamp = input.parseState.GetLong(0); expiryTicks = input.header.cmd == RespCommand.PEXPIREAT @@ -1014,7 +1083,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte break; case RespCommand.PERSIST: - recordHasEtag = false; + shouldUpdateEtag = false; oldValue.AsReadOnlySpan().CopyTo(newValue.AsSpan()); if (oldValue.MetadataSize != 0) { @@ -1115,10 +1184,13 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte // Then, set ExpireAndStop action to delete the record. CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); rmwInfo.Action = RMWAction.ExpireAndStop; + + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return false; case RespCommand.GETEX: - recordHasEtag = false; + shouldUpdateEtag = false; CopyRespTo(ref oldValue, ref output, functionsState.etagState.etagSkippedStart, functionsState.etagState.etagAccountedLength); if (input.arg1 > 0) @@ -1164,6 +1236,8 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (recordInfo.ETag) { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); + // reset etag state that may have been initialized earlier + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -1193,10 +1267,13 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.SetUsedValueLength(ref recordInfo, ref newValue, newValue.TotalSize); - if (recordHasEtag) + if (shouldUpdateEtag) { newValue.SetEtagInPayload(functionsState.etagState.etag + 1); + functionsState.etagState.ResetToDefaultVals(); } + else if (recordHadEtagPreMutation) + functionsState.etagState.ResetToDefaultVals(); return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 96217df6d2..be7dcfd042 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -58,7 +58,8 @@ public bool SingleReader( // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { - RespWriteUtils.CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -69,6 +70,11 @@ public bool SingleReader( CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending); } + if (readInfo.RecordInfo.ETag) + { + functionsState.etagState.ResetToDefaultVals(); + } + return true; } @@ -116,7 +122,8 @@ public bool ConcurrentReader( // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { - RespWriteUtils.CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); + functionsState.etagState.ResetToDefaultVals(); return true; } @@ -128,6 +135,11 @@ public bool ConcurrentReader( CopyRespToWithInput(ref input, ref value, ref dst, readInfo.IsFromPending); } + if (readInfo.RecordInfo.ETag) + { + functionsState.etagState.ResetToDefaultVals(); + } + return true; } @@ -144,7 +156,7 @@ private bool handleGetIfNotMatch(ref RawStringInput input, ref SpanByte value, r var nilResp = CmdStrings.RESP_ERRNOTFOUND; // *2\r\n: + + \r\n + var numDigitsInEtag = NumUtils.NumDigitsInLong(existingEtag); - RespWriteUtils.WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, functionsState.memoryPool, writeDirect: true); + WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, existingEtag, ref dst, functionsState.memoryPool, writeDirect: true); return true; } diff --git a/libs/server/Transaction/TxnRespCommands.cs b/libs/server/Transaction/TxnRespCommands.cs index 0f5c90b170..084b004827 100644 --- a/libs/server/Transaction/TxnRespCommands.cs +++ b/libs/server/Transaction/TxnRespCommands.cs @@ -99,7 +99,7 @@ private bool NetworkSKIP(RespCommand cmd) { // Retrieve the meta-data for the command to do basic sanity checking for command arguments // Normalize will turn internal "not-real commands" such as SETEXNX, and SETEXXX to the command info parent - cmd = cmd.Normalize(); + cmd = cmd.NormalizeForACLs(); if (!RespCommandsInfo.TryGetRespCommandInfo(cmd, out var commandInfo, txnOnly: true, logger)) { while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref dcurr, dend)) diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index 9acf7cf2b7..de4ba5bdfd 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -98,7 +98,7 @@ public void AllCommandsCovered() // Check tests against RespCommand { - IEnumerable allValues = Enum.GetValues().Select(static x => x.Normalize()).Distinct(); + IEnumerable allValues = Enum.GetValues().Select(static x => x.NormalizeForACLs()).Distinct(); IEnumerable testableValues = allValues .Except([RespCommand.NONE, RespCommand.INVALID]) diff --git a/website/docs/commands/garnet-specific.md b/website/docs/commands/garnet-specific.md index 4bec1747bf..1a31e55d29 100644 --- a/website/docs/commands/garnet-specific.md +++ b/website/docs/commands/garnet-specific.md @@ -196,13 +196,15 @@ One of the following: ### Compatibility and Behavior with Non-ETag Commands +ETags are currently not supported for servers running in Cluster mode. This will be supported soon. + Below is the expected behavior of ETag-associated key-value pairs when non-ETag commands are used. - **MSET, BITOP**: These commands will replace an existing ETag-associated key-value pair with a non-ETag key-value pair, effectively removing the ETag. - **SET**: Only if used with additional option "WITHETAG" will calling SET update the etag while inserting the new key-value pair over the existing key-value pair. -- **RENAME**: RENAME takes an option for WITHETAG. When called WITHETAG +- **RENAME**: RENAME takes an option for WITHETAG. When called WITHETAG it will rename the key with an etag if the key being renamed to did not exist, else it will increment the existing etag of the key being renamed to. - **Custom Commands**: While etag based key value pairs **can be used blindly inside of custom transactions and custom procedures**, ETag set key value pairs are **not supported to be used from inside of Custom Raw String Functions.** From 7ec36c4d1fc979ef64e7efa7b253a43adce23801 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 18:39:41 -0800 Subject: [PATCH 79/87] use const --- libs/server/Storage/Functions/MainStore/PrivateMethods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs index 5c70023675..747132d8af 100644 --- a/libs/server/Storage/Functions/MainStore/PrivateMethods.cs +++ b/libs/server/Storage/Functions/MainStore/PrivateMethods.cs @@ -678,12 +678,12 @@ static void CopyRespWithEtagData(ref SpanByte value, ref SpanByteAndMemory dst, int desiredLength = 4; ReadOnlySpan etagTruncatedVal; // get etag to write, default etag 0 for when no etag - long etag = hasEtagInVal ? value.GetEtagInPayload() : 0; // BaseEtag + long etag = hasEtagInVal ? value.GetEtagInPayload() : Constants.BaseEtag; // remove the length of the ETAG var etagAccountedValueLength = valueLength - etagSkippedStart; if (hasEtagInVal) { - etagAccountedValueLength = valueLength - sizeof(long); // EtagSize + etagAccountedValueLength = valueLength - Constants.EtagSize; } // here we know the value span has first bytes set to etag so we hardcode skipping past the bytes for the etag below From 9edc7e4446607777ed61322ad6eb470f0d530edf Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 20:28:06 -0800 Subject: [PATCH 80/87] copy struct experiment --- libs/server/Storage/Functions/EtagState.cs | 38 ++++----- .../Storage/Functions/FunctionsState.cs | 2 +- .../Storage/Functions/MainStore/RMWMethods.cs | 78 ++++++++++--------- .../Functions/MainStore/ReadMethods.cs | 12 +-- 4 files changed, 64 insertions(+), 66 deletions(-) diff --git a/libs/server/Storage/Functions/EtagState.cs b/libs/server/Storage/Functions/EtagState.cs index 3771c2ba75..460451caf5 100644 --- a/libs/server/Storage/Functions/EtagState.cs +++ b/libs/server/Storage/Functions/EtagState.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Runtime.CompilerServices; using Tsavorite.core; namespace Garnet.server @@ -8,49 +9,44 @@ namespace Garnet.server /// /// Indirection wrapper to provide a way to set offsets related to Etags and use the getters opaquely from outside. /// - public class EtagState + public readonly struct EtagState { + public EtagState() + { + } + /// /// Offset used accounting space for an etag during allocation /// - public byte etagOffsetForVarlen { get; set; } = 0; + public byte etagOffsetForVarlen { get; init; } = 0; /// /// Gives an offset used to opaquely work with Etag in a payload. By calling this you can skip past the etag if it is present. /// - public byte etagSkippedStart { get; private set; } = 0; + public byte etagSkippedStart { get; init; } = 0; /// /// Resp response methods depend on the value for end being -1 or length of the payload. This field lets you work with providing the end opaquely. /// - public int etagAccountedLength { get; private set; } = -1; + public int etagAccountedLength { get; init; } = -1; /// /// Field provides access to getting an Etag from a record, hiding whether it is actually present or not. /// - public long etag { get; private set; } = Constants.BaseEtag; + public long etag { get; init; } = Constants.BaseEtag; /// /// Sets the values to indicate the presence of an Etag as a part of the payload value /// /// The SpanByte for the record - public void SetValsForRecordWithEtag(ref SpanByte value) + public static EtagState SetValsForRecordWithEtag(ref SpanByte value) => new EtagState { - etagOffsetForVarlen = Constants.EtagSize; - etagSkippedStart = Constants.EtagSize; - etagAccountedLength = value.LengthWithoutMetadata; - etag = value.GetEtagInPayload(); - } + etagOffsetForVarlen = Constants.EtagSize, + etagSkippedStart = Constants.EtagSize, + etagAccountedLength = value.LengthWithoutMetadata, + etag = value.GetEtagInPayload() + }; - /// - /// Resets the values back to default values so that state between operations does not leak - /// - public void ResetToDefaultVals() - { - etagOffsetForVarlen = 0; - etagSkippedStart = 0; - etagAccountedLength = -1; - etag = Constants.BaseEtag; - } + public static EtagState ResetState() => new EtagState(); } } \ No newline at end of file diff --git a/libs/server/Storage/Functions/FunctionsState.cs b/libs/server/Storage/Functions/FunctionsState.cs index ca2eefddce..be097ca976 100644 --- a/libs/server/Storage/Functions/FunctionsState.cs +++ b/libs/server/Storage/Functions/FunctionsState.cs @@ -18,7 +18,7 @@ internal sealed class FunctionsState public readonly MemoryPool memoryPool; public readonly CacheSizeTracker objectStoreSizeTracker; public readonly GarnetObjectSerializer garnetObjectSerializer; - public readonly EtagState etagState; + public EtagState etagState; public bool StoredProcMode; public FunctionsState(TsavoriteLog appendOnlyFile, WatchVersionMap watchVersionMap, CustomCommandManager customCommandManager, diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 1f79e8c5d4..13a5fb6da9 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -40,7 +40,8 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.SET: case RespCommand.SETEXNX: case RespCommand.SETKEEPTTL: - this.functionsState.etagState.etagOffsetForVarlen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); + byte extraAllocationLen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); + this.functionsState.etagState = this.functionsState.etagState with {etagOffsetForVarlen = extraAllocationLen }; return true; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) @@ -101,7 +102,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag value.SetEtagInPayload(Constants.BaseEtag + 1); - functionsState.etagState.SetValsForRecordWithEtag(ref value); + functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX CopyRespNumber(Constants.BaseEtag + 1, ref output); } @@ -118,7 +119,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB { recordInfo.SetHasETag(); value.SetEtagInPayload(Constants.BaseEtag + 1); - functionsState.etagState.SetValsForRecordWithEtag(ref value); + functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); // Copy initial etag to output CopyRespNumber(Constants.BaseEtag + 1, ref output); } @@ -256,7 +257,7 @@ public void PostInitialUpdater(ref SpanByte key, ref RawStringInput input, ref S { // reset etag state set at need initial update if (input.header.cmd is (RespCommand.SET or RespCommand.SETEXNX or RespCommand.SETKEEPTTL)) - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); functionsState.watchVersionMap.IncrementVersion(rmwInfo.KeyHash); if (functionsState.appendOnlyFile != null) @@ -296,7 +297,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re bool shouldUpdateEtag = hadRecordPreMutation; if (shouldUpdateEtag) { - functionsState.etagState.SetValsForRecordWithEtag(ref value); + functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); } switch (cmd) @@ -315,7 +316,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // reset etag state after done using - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); // Nothing is set because being in this block means NX was already violated return true; case RespCommand.SETIFMATCH: @@ -324,7 +325,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyRespWithEtagData(ref value, ref output, shouldUpdateEtag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); // reset etag state after done using - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -361,7 +362,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); // reset etag state after done using - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); // early return since we already updated the ETag return true; case RespCommand.SET: @@ -381,14 +382,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re nextUpdateEtagOffset = Constants.EtagSize; shouldUpdateEtag = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; + this.functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen = Constants.EtagSize}; } else { shouldUpdateEtag = false; // nextUpdate will remove etag but currently there is an etag nextUpdateEtagOffset = 0; - this.functionsState.etagState.etagOffsetForVarlen = 0; + this.functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen = 0}; } } @@ -425,7 +426,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // withetag flag means we need to write etag back to the output buffer CopyRespNumber(functionsState.etagState.etag + 1, ref output); // reset etag state after done using - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); // early return since we already updated etag return true; } @@ -454,14 +455,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re nextUpdateEtagOffset = Constants.EtagSize; shouldUpdateEtag = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; + functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen =Constants.EtagSize}; } else { - shouldUpdateEtag = true; + shouldUpdateEtag = false; // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; - functionsState.etagState.etagOffsetForVarlen = 0; + functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen = 0}; } } @@ -493,7 +494,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // withetag flag means we need to write etag back to the output buffer CopyRespNumber(functionsState.etagState.etag + 1, ref output); // reset etag state after done using - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); // early return since we already updated etag return true; } @@ -516,7 +517,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var expireOption = (ExpireOption)input.arg1; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; @@ -534,7 +535,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re expireOption = (ExpireOption)input.arg1; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; @@ -554,7 +555,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // does not update etag // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; case RespCommand.INCR: @@ -589,7 +590,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat, functionsState.etagState.etagSkippedStart)) @@ -632,7 +633,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); // etag not updated return true; } @@ -647,7 +648,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { *output.SpanByte.ToPointer() = (byte)0xFF; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -669,7 +670,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!HyperLogLog.DefaultHLL.IsValidHYLL(dstHLL, value.Length)) { // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); //InvalidType *(long*)output.SpanByte.ToPointer() = -1; return true; @@ -722,13 +723,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.UnmarkExtraMetadata(); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } } // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; case RespCommand.APPEND: @@ -739,7 +740,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyValueLengthToOutput(ref value, ref output, functionsState.etagState.etagSkippedStart); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -751,7 +752,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -808,7 +809,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (hadRecordPreMutation) { // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); } return true; @@ -831,7 +832,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; } @@ -843,7 +844,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB rmwInfo.Action = RMWAction.ExpireAndResume; rmwInfo.RecordInfo.ClearHasETag(); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; } @@ -861,7 +862,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB } // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; case RespCommand.SETEXXX: // Expired data, return false immediately so we do not set, since it does not exist @@ -871,7 +872,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB rmwInfo.RecordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndStop; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; } return true; @@ -882,7 +883,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; } (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); @@ -905,7 +906,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte recordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndResume; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; } @@ -918,7 +919,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (shouldUpdateEtag) { // during checkpointing we might skip the inplace calls and go directly to copy update so we need to initialize here if needed - functionsState.etagState.SetValsForRecordWithEtag(ref oldValue); + functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref oldValue); } switch (cmd) @@ -974,6 +975,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte { // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; + shouldUpdateEtag = false; recordInfo.ClearHasETag(); } } @@ -1186,7 +1188,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.Action = RMWAction.ExpireAndStop; // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return false; case RespCommand.GETEX: @@ -1237,7 +1239,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -1270,10 +1272,10 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (shouldUpdateEtag) { newValue.SetEtagInPayload(functionsState.etagState.etag + 1); - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); } else if (recordHadEtagPreMutation) - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index be7dcfd042..21356b7af0 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -52,14 +52,14 @@ public bool SingleReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState.SetValsForRecordWithEtag(ref value); + functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -72,7 +72,7 @@ public bool SingleReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); } return true; @@ -116,14 +116,14 @@ public bool ConcurrentReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState.SetValsForRecordWithEtag(ref value); + functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); return true; } @@ -137,7 +137,7 @@ public bool ConcurrentReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState.ResetToDefaultVals(); + functionsState.etagState = EtagState.ResetState(); } return true; From f3be68090423644a8e9df589ade45c3f7e31bf4f Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 20:53:13 -0800 Subject: [PATCH 81/87] Experiment with mutable struct --- libs/server/Storage/Functions/EtagState.cs | 32 ++++--- .../Storage/Functions/MainStore/RMWMethods.cs | 86 +++++++++---------- .../Functions/MainStore/ReadMethods.cs | 12 +-- 3 files changed, 67 insertions(+), 63 deletions(-) diff --git a/libs/server/Storage/Functions/EtagState.cs b/libs/server/Storage/Functions/EtagState.cs index 460451caf5..c9f043f7fc 100644 --- a/libs/server/Storage/Functions/EtagState.cs +++ b/libs/server/Storage/Functions/EtagState.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System.Runtime.CompilerServices; using Tsavorite.core; namespace Garnet.server @@ -9,7 +8,7 @@ namespace Garnet.server /// /// Indirection wrapper to provide a way to set offsets related to Etags and use the getters opaquely from outside. /// - public readonly struct EtagState + public struct EtagState { public EtagState() { @@ -18,35 +17,40 @@ public EtagState() /// /// Offset used accounting space for an etag during allocation /// - public byte etagOffsetForVarlen { get; init; } = 0; + public byte etagOffsetForVarlen { get; set; } = 0; /// /// Gives an offset used to opaquely work with Etag in a payload. By calling this you can skip past the etag if it is present. /// - public byte etagSkippedStart { get; init; } = 0; + public byte etagSkippedStart { get; private set; } = 0; /// /// Resp response methods depend on the value for end being -1 or length of the payload. This field lets you work with providing the end opaquely. /// - public int etagAccountedLength { get; init; } = -1; + public int etagAccountedLength { get; private set; } = -1; /// /// Field provides access to getting an Etag from a record, hiding whether it is actually present or not. /// - public long etag { get; init; } = Constants.BaseEtag; + public long etag { get; private set; } = Constants.BaseEtag; /// /// Sets the values to indicate the presence of an Etag as a part of the payload value /// - /// The SpanByte for the record - public static EtagState SetValsForRecordWithEtag(ref SpanByte value) => new EtagState + public static void SetValsForRecordWithEtag(ref EtagState curr, ref SpanByte value) { - etagOffsetForVarlen = Constants.EtagSize, - etagSkippedStart = Constants.EtagSize, - etagAccountedLength = value.LengthWithoutMetadata, - etag = value.GetEtagInPayload() - }; + curr.etagOffsetForVarlen = Constants.EtagSize; + curr.etagSkippedStart = Constants.EtagSize; + curr.etagAccountedLength = value.LengthWithoutMetadata; + curr.etag = value.GetEtagInPayload(); + } - public static EtagState ResetState() => new EtagState(); + public static void ResetState(ref EtagState curr) + { + curr.etagOffsetForVarlen = 0; + curr.etagSkippedStart = 0; + curr.etag = Constants.BaseEtag; + curr.etagAccountedLength = -1; + } } } \ No newline at end of file diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 13a5fb6da9..4c275d965c 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -41,7 +41,7 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.SETEXNX: case RespCommand.SETKEEPTTL: byte extraAllocationLen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); - this.functionsState.etagState = this.functionsState.etagState with {etagOffsetForVarlen = extraAllocationLen }; + this.functionsState.etagState.etagOffsetForVarlen = extraAllocationLen; return true; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) @@ -102,7 +102,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB recordInfo.SetHasETag(); // the increment on initial etag is for satisfying the variant that any key with no etag is the same as a zero'd etag value.SetEtagInPayload(Constants.BaseEtag + 1); - functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); + EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref value); // Copy initial etag to output only for SET + WITHETAG and not SET NX or XX CopyRespNumber(Constants.BaseEtag + 1, ref output); } @@ -119,7 +119,7 @@ public bool InitialUpdater(ref SpanByte key, ref RawStringInput input, ref SpanB { recordInfo.SetHasETag(); value.SetEtagInPayload(Constants.BaseEtag + 1); - functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); + EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref value); // Copy initial etag to output CopyRespNumber(Constants.BaseEtag + 1, ref output); } @@ -257,7 +257,7 @@ public void PostInitialUpdater(ref SpanByte key, ref RawStringInput input, ref S { // reset etag state set at need initial update if (input.header.cmd is (RespCommand.SET or RespCommand.SETEXNX or RespCommand.SETKEEPTTL)) - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); functionsState.watchVersionMap.IncrementVersion(rmwInfo.KeyHash); if (functionsState.appendOnlyFile != null) @@ -297,7 +297,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re bool shouldUpdateEtag = hadRecordPreMutation; if (shouldUpdateEtag) { - functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); + EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref value); } switch (cmd) @@ -316,7 +316,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // reset etag state after done using - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); // Nothing is set because being in this block means NX was already violated return true; case RespCommand.SETIFMATCH: @@ -325,7 +325,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyRespWithEtagData(ref value, ref output, shouldUpdateEtag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); // reset etag state after done using - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -362,7 +362,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var numDigitsInEtag = NumUtils.NumDigitsInLong(newEtag); WriteValAndEtagToDst(4 + 1 + numDigitsInEtag + 2 + nilResp.Length, ref nilResp, newEtag, ref output, functionsState.memoryPool, writeDirect: true); // reset etag state after done using - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); // early return since we already updated the ETag return true; case RespCommand.SET: @@ -382,14 +382,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re nextUpdateEtagOffset = Constants.EtagSize; shouldUpdateEtag = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - this.functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen = Constants.EtagSize}; + this.functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; } else { shouldUpdateEtag = false; // nextUpdate will remove etag but currently there is an etag nextUpdateEtagOffset = 0; - this.functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen = 0}; + this.functionsState.etagState.etagOffsetForVarlen = 0; } } @@ -426,7 +426,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // withetag flag means we need to write etag back to the output buffer CopyRespNumber(functionsState.etagState.etag + 1, ref output); // reset etag state after done using - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); // early return since we already updated etag return true; } @@ -455,14 +455,14 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re nextUpdateEtagOffset = Constants.EtagSize; shouldUpdateEtag = true; // if something is going to go past this into copy we need to provide offset management for its varlen during allocation - functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen =Constants.EtagSize}; + this.functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; } else { shouldUpdateEtag = false; // nextUpdate will remove etag but currentyly there is an etag nextUpdateEtagOffset = 0; - functionsState.etagState = functionsState.etagState with { etagOffsetForVarlen = 0}; + this.functionsState.etagState.etagOffsetForVarlen = 0; } } @@ -494,7 +494,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re // withetag flag means we need to write etag back to the output buffer CopyRespNumber(functionsState.etagState.etag + 1, ref output); // reset etag state after done using - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); // early return since we already updated etag return true; } @@ -517,7 +517,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re var expireOption = (ExpireOption)input.arg1; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; @@ -535,7 +535,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re expireOption = (ExpireOption)input.arg1; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); if (!EvaluateExpireInPlace(expireOption, expiryExists, expiryTicks, ref value, ref output)) return false; @@ -555,7 +555,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re } // does not update etag // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; case RespCommand.INCR: @@ -590,7 +590,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { output.SpanByte.AsSpan()[0] = (byte)OperationError.INVALID_TYPE; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } if (!TryInPlaceUpdateNumber(ref value, ref output, ref rmwInfo, ref recordInfo, incrByFloat, functionsState.etagState.etagSkippedStart)) @@ -633,7 +633,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyDefaultResp(CmdStrings.RESP_ERRNOTFOUND, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); // etag not updated return true; } @@ -648,7 +648,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { *output.SpanByte.ToPointer() = (byte)0xFF; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -670,7 +670,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (!HyperLogLog.DefaultHLL.IsValidHYLL(dstHLL, value.Length)) { // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); //InvalidType *(long*)output.SpanByte.ToPointer() = -1; return true; @@ -723,13 +723,13 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re value.UnmarkExtraMetadata(); rmwInfo.SetUsedValueLength(ref recordInfo, ref value, value.TotalSize); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } } // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; case RespCommand.APPEND: @@ -740,7 +740,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyValueLengthToOutput(ref value, ref output, functionsState.etagState.etagSkippedStart); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -752,7 +752,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -809,7 +809,7 @@ private bool InPlaceUpdaterWorker(ref SpanByte key, ref RawStringInput input, re if (hadRecordPreMutation) { // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); } return true; @@ -832,7 +832,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB { CopyRespWithEtagData(ref oldValue, ref output, hasEtagInVal: rmwInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return false; } @@ -844,7 +844,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB rmwInfo.Action = RMWAction.ExpireAndResume; rmwInfo.RecordInfo.ClearHasETag(); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return false; } @@ -862,7 +862,7 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB } // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return false; case RespCommand.SETEXXX: // Expired data, return false immediately so we do not set, since it does not exist @@ -872,20 +872,20 @@ public bool NeedCopyUpdate(ref SpanByte key, ref RawStringInput input, ref SpanB rmwInfo.RecordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndStop; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return false; } return true; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) { - if (rmwInfo.RecordInfo.ETag) - { - CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); - // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); - return false; - } + if (rmwInfo.RecordInfo.ETag) + { + CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); + // reset etag state that may have been initialized earlier + EtagState.ResetState(ref functionsState.etagState); + return false; + } (IMemoryOwner Memory, int Length) outp = (output.Memory, 0); var ret = functionsState.GetCustomCommandFunctions((ushort)input.header.cmd) .NeedCopyUpdate(key.AsReadOnlySpan(), ref input, oldValue.AsReadOnlySpan(functionsState.etagState.etagSkippedStart), ref outp); @@ -906,7 +906,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte recordInfo.ClearHasETag(); rmwInfo.Action = RMWAction.ExpireAndResume; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return false; } @@ -919,7 +919,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (shouldUpdateEtag) { // during checkpointing we might skip the inplace calls and go directly to copy update so we need to initialize here if needed - functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref oldValue); + EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref oldValue); } switch (cmd) @@ -1188,7 +1188,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte rmwInfo.Action = RMWAction.ExpireAndStop; // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return false; case RespCommand.GETEX: @@ -1239,7 +1239,7 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte { CopyDefaultResp(CmdStrings.RESP_ERR_ETAG_ON_CUSTOM_PROC, ref output); // reset etag state that may have been initialized earlier - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -1272,10 +1272,10 @@ public bool CopyUpdater(ref SpanByte key, ref RawStringInput input, ref SpanByte if (shouldUpdateEtag) { newValue.SetEtagInPayload(functionsState.etagState.etag + 1); - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); } else if (recordHadEtagPreMutation) - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } diff --git a/libs/server/Storage/Functions/MainStore/ReadMethods.cs b/libs/server/Storage/Functions/MainStore/ReadMethods.cs index 21356b7af0..cbdf3f8776 100644 --- a/libs/server/Storage/Functions/MainStore/ReadMethods.cs +++ b/libs/server/Storage/Functions/MainStore/ReadMethods.cs @@ -52,14 +52,14 @@ public bool SingleReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); + EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref value); } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -72,7 +72,7 @@ public bool SingleReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); } return true; @@ -116,14 +116,14 @@ public bool ConcurrentReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState = EtagState.SetValsForRecordWithEtag(ref value); + EtagState.SetValsForRecordWithEtag(ref functionsState.etagState, ref value); } // Unless the command explicitly asks for the ETag in response, we do not write back the ETag if (cmd is (RespCommand.GETWITHETAG or RespCommand.GETIFNOTMATCH)) { CopyRespWithEtagData(ref value, ref dst, readInfo.RecordInfo.ETag, functionsState.etagState.etagSkippedStart, functionsState.memoryPool); - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); return true; } @@ -137,7 +137,7 @@ public bool ConcurrentReader( if (readInfo.RecordInfo.ETag) { - functionsState.etagState = EtagState.ResetState(); + EtagState.ResetState(ref functionsState.etagState); } return true; From a15c460ce40ecf7b8da974b7ae74b8984653df38 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 21:22:46 -0800 Subject: [PATCH 82/87] minor fix --- libs/server/Storage/Functions/MainStore/RMWMethods.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/server/Storage/Functions/MainStore/RMWMethods.cs b/libs/server/Storage/Functions/MainStore/RMWMethods.cs index 4c275d965c..3ae61a4006 100644 --- a/libs/server/Storage/Functions/MainStore/RMWMethods.cs +++ b/libs/server/Storage/Functions/MainStore/RMWMethods.cs @@ -40,8 +40,10 @@ public bool NeedInitialUpdate(ref SpanByte key, ref RawStringInput input, ref Sp case RespCommand.SET: case RespCommand.SETEXNX: case RespCommand.SETKEEPTTL: - byte extraAllocationLen = (byte)(input.header.CheckWithEtagFlag() ? Constants.EtagSize : 0); - this.functionsState.etagState.etagOffsetForVarlen = extraAllocationLen; + if (input.header.CheckWithEtagFlag()) + { + this.functionsState.etagState.etagOffsetForVarlen = Constants.EtagSize; + } return true; default: if (input.header.cmd > RespCommandExtensions.LastValidCommand) From 5808bd18b7252eabe718e818eb7db7426a470e5b Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 22:58:07 -0800 Subject: [PATCH 83/87] add comment --- libs/common/RespWriteUtils.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libs/common/RespWriteUtils.cs b/libs/common/RespWriteUtils.cs index 81159c5f42..db3b432eb9 100644 --- a/libs/common/RespWriteUtils.cs +++ b/libs/common/RespWriteUtils.cs @@ -698,6 +698,15 @@ public static bool WriteArrayWithNullElements(int len, ref byte* curr, byte* end return true; } + /// + /// Writes an array consisting of an ETag followed by a Bulk string value into the buffer. + /// NOTE: Caller should make sure there is enough space in the buffer for sending the etag, and value array. + /// + /// + /// + /// + /// + /// public static void WriteEtagValArray(long etag, ref ReadOnlySpan value, ref byte* curr, byte* end, bool writeDirect) { // Writes a Resp encoded Array of Integer for ETAG as first element, and bulk string for value as second element From eb77411d1580dd516f4fb717cc80ce9c9c17a835 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Thu, 9 Jan 2025 23:25:17 -0800 Subject: [PATCH 84/87] remove unnecessary change --- libs/server/Resp/Bitmap/BitmapManagerBitPos.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs index ee16f5e1f4..aba15d649a 100644 --- a/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs +++ b/libs/server/Resp/Bitmap/BitmapManagerBitPos.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using System.Diagnostics; -using System.Numerics; using System.Runtime.Intrinsics.X86; namespace Garnet.server @@ -178,7 +177,7 @@ private static long BitPosByte(byte* value, byte bSetVal, long startOffset, long if (payload == mask) return pos + 0; - pos += (long)BitOperations.LeadingZeroCount((ulong)payload); + pos += (long)Lzcnt.X64.LeadingZeroCount((ulong)payload); return pos; } From a7d63e1d8352af6d423b9c1dc3ed530073c3484d Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 10 Jan 2025 00:04:41 -0800 Subject: [PATCH 85/87] prove that I am not crazy --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d633329ac9..d0017615df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ ubuntu-22.04, ubuntu-latest, windows-latest ] framework: [ 'net8.0' ] configuration: [ 'Debug', 'Release' ] if: needs.changes.outputs.tsavorite == 'true' From 28c990497b72415325b4b9c4addf45b023164a31 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 10 Jan 2025 00:15:23 -0800 Subject: [PATCH 86/87] LETS GOI FIX UBUNTU ISSUE --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0017615df..f8ad9862d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-22.04, ubuntu-latest, windows-latest ] + os: [ ubuntu-22.04, windows-latest ] framework: [ 'net8.0' ] configuration: [ 'Debug', 'Release' ] if: needs.changes.outputs.tsavorite == 'true' @@ -123,7 +123,7 @@ jobs: uses: actions/checkout@v4 - name: Set environment variable for Linux run: echo "RunAzureTests=yes" >> $GITHUB_ENV - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} - name: Set environment variable for Windows run: echo ("RunAzureTests=yes") >> $env:GITHUB_ENV if: ${{ matrix.os == 'windows-latest' }} From 7138f2986ce12e60b5ebb82016218ce4e01b1640 Mon Sep 17 00:00:00 2001 From: Hamdaan Khalid Date: Fri, 10 Jan 2025 10:24:59 -0800 Subject: [PATCH 87/87] add feedback --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8ad9862d9..5f17bb7e4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,16 +114,20 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-22.04, windows-latest ] + os: [ ubuntu-latest, windows-latest ] framework: [ 'net8.0' ] configuration: [ 'Debug', 'Release' ] if: needs.changes.outputs.tsavorite == 'true' steps: - name: Check out code uses: actions/checkout@v4 + - name: Set workaround for libaio on Ubuntu 24.04 (see https://askubuntu.com/questions/1512196/libaio1-on-noble/1512197#1512197) + run: | + sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1 + if: ${{ matrix.os == 'ubuntu-latest' }} - name: Set environment variable for Linux run: echo "RunAzureTests=yes" >> $GITHUB_ENV - if: ${{ matrix.os == 'ubuntu-22.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} - name: Set environment variable for Windows run: echo ("RunAzureTests=yes") >> $env:GITHUB_ENV if: ${{ matrix.os == 'windows-latest' }}