diff --git a/libs/server/Objects/ItemBroker/CollectionItemBroker.cs b/libs/server/Objects/ItemBroker/CollectionItemBroker.cs index f33fc61a4bb..0cec4dff5d5 100644 --- a/libs/server/Objects/ItemBroker/CollectionItemBroker.cs +++ b/libs/server/Objects/ItemBroker/CollectionItemBroker.cs @@ -235,7 +235,7 @@ private void InitializeObserver(CollectionItemObserver observer, byte[][] keys) // If the key already has a non-empty observer queue, it does not have an item to retrieve // Otherwise, try to retrieve next available item if ((KeysToObservers.ContainsKey(key) && KeysToObservers[key].Count > 0) || - !TryGetResult(key, observer.Session.storageSession, observer.Command, observer.CommandArgs, + !TryGetResult(key, observer.Session.storageSession, observer.Command, observer.CommandArgs, true, out _, out var result)) continue; // An item was found - set the observer result and return @@ -291,7 +291,7 @@ private bool TryAssignItemFromKey(byte[] key) } // Try to get next available item from object stored in key - if (!TryGetResult(key, observer.Session.storageSession, observer.Command, observer.CommandArgs, + if (!TryGetResult(key, observer.Session.storageSession, observer.Command, observer.CommandArgs, false, out var currCount, out var result)) { // If unsuccessful getting next item but there is at least one item in the collection, @@ -436,7 +436,13 @@ private static unsafe bool TryGetNextSetObjects(byte[] key, SortedSetObject sort } } - private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, RespCommand command, ArgSlice[] cmdArgs, out int currCount, out CollectionItemResult result) + /// + /// Try to get available item(s) from sorted set object based on command type and arguments + /// When run from initializer (initial = true), can return WRONGTYPE errors + /// + private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, + RespCommand command, ArgSlice[] cmdArgs, bool initial, + out int currCount, out CollectionItemResult result) { currCount = default; result = default; @@ -449,6 +455,7 @@ private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, Resp _ => throw new NotSupportedException() }; + var asKey = storageSession.scratchBufferManager.CreateArgSlice(key); ArgSlice dstKey = default; if (command == RespCommand.BLMOVE) { @@ -460,32 +467,42 @@ private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, Resp { Debug.Assert(storageSession.txnManager.state == TxnState.None); createTransaction = true; - var asKey = storageSession.scratchBufferManager.CreateArgSlice(key); + if (initial) + storageSession.txnManager.SaveKeyEntryToLock(asKey, false, LockType.Exclusive); storageSession.txnManager.SaveKeyEntryToLock(asKey, true, LockType.Exclusive); if (command == RespCommand.BLMOVE) { + if (initial) + storageSession.txnManager.SaveKeyEntryToLock(dstKey, false, LockType.Exclusive); storageSession.txnManager.SaveKeyEntryToLock(dstKey, true, LockType.Exclusive); } _ = storageSession.txnManager.Run(true); } + var lockableContext = storageSession.txnManager.LockableContext; var objectLockableContext = storageSession.txnManager.ObjectStoreLockableContext; try { // Get the object stored at key var statusOp = storageSession.GET(key, out var osObject, ref objectLockableContext); - if (statusOp == GarnetStatus.NOTFOUND) return false; - - IGarnetObject dstObj = null; - byte[] arrDstKey = default; - if (command == RespCommand.BLMOVE) + if (statusOp == GarnetStatus.NOTFOUND) { - arrDstKey = dstKey.ToArray(); - var dstStatusOp = storageSession.GET(arrDstKey, out var osDstObject, ref objectLockableContext); - if (dstStatusOp != GarnetStatus.NOTFOUND) dstObj = osDstObject.GarnetObject; + if (!initial) + return false; + + // Check the string store as well to see if WRONGTYPE should be returned. + statusOp = storageSession.GET(asKey, out ArgSlice _, ref lockableContext); + + if (statusOp != GarnetStatus.NOTFOUND) + { + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; + } + + return false; } // Check for type match between the observer and the actual object type @@ -494,29 +511,64 @@ private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, Resp { case ListObject listObj: currCount = listObj.LnkList.Count; - if (objectType != GarnetObjectType.List) return false; - if (currCount == 0) return false; + if (objectType != GarnetObjectType.List) + { + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; + } + if (currCount == 0) + return false; + var isSuccessful = false; switch (command) { case RespCommand.BLPOP: case RespCommand.BRPOP: - var isSuccessful = TryGetNextListItem(listObj, command, out var nextItem); + isSuccessful = TryGetNextListItem(listObj, command, out var nextItem); result = new CollectionItemResult(key, nextItem); - return isSuccessful; + break; case RespCommand.BLMOVE: + var arrDstKey = dstKey.ToArray(); + var dstStatusOp = storageSession.GET(arrDstKey, out var osDstObject, ref objectLockableContext); + ListObject dstList; var newObj = false; - if (dstObj == null) + + if (dstStatusOp != GarnetStatus.NOTFOUND) { - dstList = new ListObject(); - newObj = true; + var dstObj = osDstObject.GarnetObject; + + if (dstObj == null) + { + dstList = new ListObject(); + newObj = true; + } + else if (dstObj is ListObject tmpDstList) + { + dstList = tmpDstList; + } + else + { + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; + } } - else if (dstObj is ListObject tmpDstList) + else { - dstList = tmpDstList; + if (initial) + { + // Check string store for wrongtype errors on initial run. + dstStatusOp = storageSession.GET(dstKey, out ArgSlice _, ref lockableContext); + if (dstStatusOp != GarnetStatus.NOTFOUND) + { + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; + } + } + + dstList = new ListObject(); + newObj = true; } - else return false; isSuccessful = TryMoveNextListItem(listObj, dstList, (OperationDirection)cmdArgs[1].ReadOnlySpan[0], (OperationDirection)cmdArgs[2].ReadOnlySpan[0], out nextItem); @@ -527,8 +579,7 @@ private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, Resp isSuccessful = storageSession.SET(arrDstKey, dstList, ref objectLockableContext) == GarnetStatus.OK; } - - return isSuccessful; + break; case RespCommand.BLMPOP: var popDirection = (OperationDirection)cmdArgs[0].ReadOnlySpan[0]; var popCount = *(int*)(cmdArgs[1].ptr); @@ -537,25 +588,44 @@ private unsafe bool TryGetResult(byte[] key, StorageSession storageSession, Resp var items = new byte[popCount][]; for (var i = 0; i < popCount; i++) { - var _ = TryGetNextListItem(listObj, popDirection == OperationDirection.Left ? RespCommand.BLPOP : RespCommand.BRPOP, out items[i]); // Return can be ignored because it is guaranteed to return true + // Return can be ignored because it is guaranteed to return true + _ = TryGetNextListItem(listObj, popDirection == OperationDirection.Left ? RespCommand.BLPOP : RespCommand.BRPOP, out items[i]); } result = new CollectionItemResult(key, items); - return true; + isSuccessful = true; + break; default: - return false; + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; + } + + if (isSuccessful && listObj.LnkList.Count == 0) + { + _ = storageSession.EXPIRE(asKey, TimeSpan.Zero, out _, StoreType.Object, ExpireOption.None, + ref lockableContext, ref objectLockableContext); } + return isSuccessful; case SortedSetObject setObj: currCount = setObj.Count(); if (objectType != GarnetObjectType.SortedSet) - return false; + { + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; + } if (currCount == 0) return false; - return TryGetNextSetObjects(key, setObj, currCount, command, cmdArgs, out result); - + isSuccessful = TryGetNextSetObjects(key, setObj, currCount, command, cmdArgs, out result); + if (isSuccessful && setObj.Count() == 0) + { + _ = storageSession.EXPIRE(asKey, TimeSpan.Zero, out _, StoreType.Object, ExpireOption.None, + ref lockableContext, ref objectLockableContext); + } + return isSuccessful; default: - return false; + result = new CollectionItemResult(GarnetStatus.WRONGTYPE); + return initial; } } finally diff --git a/libs/server/Objects/ItemBroker/CollectionItemResult.cs b/libs/server/Objects/ItemBroker/CollectionItemResult.cs index 4a980a8e639..dbcff00e2b5 100644 --- a/libs/server/Objects/ItemBroker/CollectionItemResult.cs +++ b/libs/server/Objects/ItemBroker/CollectionItemResult.cs @@ -8,30 +8,39 @@ namespace Garnet.server /// internal readonly struct CollectionItemResult { + public CollectionItemResult(GarnetStatus status) + { + Status = status; + } + public CollectionItemResult(byte[] key, byte[] item) { Key = key; Item = item; + Status = key == default ? GarnetStatus.NOTFOUND : GarnetStatus.OK; } public CollectionItemResult(byte[] key, byte[][] items) { Key = key; Items = items; + Status = key == default ? GarnetStatus.NOTFOUND : GarnetStatus.OK; } public CollectionItemResult(byte[] key, double score, byte[] item) { Key = key; - Score = score; Item = item; + Score = score; + Status = key == default ? GarnetStatus.NOTFOUND : GarnetStatus.OK; } public CollectionItemResult(byte[] key, double[] scores, byte[][] items) { Key = key; - Scores = scores; Items = items; + Scores = scores; + Status = key == default ? GarnetStatus.NOTFOUND : GarnetStatus.OK; } private CollectionItemResult(bool isForceUnblocked) @@ -40,9 +49,9 @@ private CollectionItemResult(bool isForceUnblocked) } /// - /// True if item was found + /// Result status /// - internal bool Found => Key != default; + internal GarnetStatus Status { get; } /// /// Key of collection from which item was retrieved diff --git a/libs/server/Resp/Objects/ListCommands.cs b/libs/server/Resp/Objects/ListCommands.cs index 94f54ac9402..8bd0fb418a2 100644 --- a/libs/server/Resp/Objects/ListCommands.cs +++ b/libs/server/Resp/Objects/ListCommands.cs @@ -294,9 +294,12 @@ private bool ListBlockingPop(RespCommand command) if (!parseState.TryGetDouble(parseState.Count - 1, out var timeout)) { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT, ref dcurr, dend)) - SendAndReset(); - return true; + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT); + } + + if (timeout < 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_IS_NEGATIVE); } if (storeWrapper.itemBroker == null) @@ -311,21 +314,27 @@ private bool ListBlockingPop(RespCommand command) return true; } - if (!result.Found) + switch (result.Status) { - while (!RespWriteUtils.TryWriteNullArray(ref dcurr, dend)) - SendAndReset(); - } - else - { - while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) - SendAndReset(); + case GarnetStatus.OK: + while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(new Span(result.Key), ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(new Span(result.Key), ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(new Span(result.Item), ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(new Span(result.Item), ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.NOTFOUND: + default: + while (!RespWriteUtils.TryWriteNullArray(ref dcurr, dend)) + SendAndReset(); + break; } return true; @@ -345,9 +354,12 @@ private unsafe bool ListBlockingMove() if (!parseState.TryGetDouble(4, out var timeout)) { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT, ref dcurr, dend)) - SendAndReset(); - return true; + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT); + } + + if (timeout < 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_IS_NEGATIVE); } return ListBlockingMove(srcKey, dstKey, srcDir, dstDir, timeout); @@ -371,9 +383,12 @@ private bool ListBlockingPopPush() if (!parseState.TryGetDouble(2, out var timeout)) { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT, ref dcurr, dend)) - SendAndReset(); - return true; + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT); + } + + if (timeout < 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_IS_NEGATIVE); } return ListBlockingMove(srcKey, dstKey, rightOption, leftOption, timeout); @@ -405,14 +420,20 @@ private bool ListBlockingMove(ArgSlice srcKey, ArgSlice dstKey, ArgSlice srcDir, var result = storeWrapper.itemBroker.MoveCollectionItemAsync(RespCommand.BLMOVE, srcKey.ToArray(), this, timeout, cmdArgs).Result; - if (!result.Found) + switch (result.Status) { - WriteNull(); - } - else - { - while (!RespWriteUtils.TryWriteBulkString(new Span(result.Item), ref dcurr, dend)) - SendAndReset(); + case GarnetStatus.OK: + while (!RespWriteUtils.TryWriteBulkString(new Span(result.Item), ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.NOTFOUND: + default: + WriteNull(); + break; } return true; @@ -925,6 +946,11 @@ private unsafe bool ListBlockingPopMultiple() return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT); } + if (timeout < 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_IS_NEGATIVE); + } + // Read count of keys if (!parseState.TryGetInt(currTokenId++, out var numKeys)) { @@ -987,28 +1013,36 @@ private unsafe bool ListBlockingPopMultiple() { while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_UNBLOCKED_CLIENT_VIA_CLIENT_UNBLOCK, ref dcurr, dend)) SendAndReset(); - } - - if (!result.Found) - { - WriteNull(); return true; } - while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) - SendAndReset(); + switch (result.Status) + { + case GarnetStatus.OK: + while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(result.Key, ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(result.Key, ref dcurr, dend)) + SendAndReset(); - var elements = result.Items; - while (!RespWriteUtils.TryWriteArrayLength(elements.Length, ref dcurr, dend)) - SendAndReset(); + var elements = result.Items; + while (!RespWriteUtils.TryWriteArrayLength(elements.Length, ref dcurr, dend)) + SendAndReset(); - foreach (var element in elements) - { - while (!RespWriteUtils.TryWriteBulkString(element, ref dcurr, dend)) - SendAndReset(); + foreach (var element in elements) + { + while (!RespWriteUtils.TryWriteBulkString(element, ref dcurr, dend)) + SendAndReset(); + } + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.NOTFOUND: + default: + WriteNull(); + break; } return true; diff --git a/libs/server/Resp/Objects/SortedSetCommands.cs b/libs/server/Resp/Objects/SortedSetCommands.cs index d0474a7c847..61859e5fa76 100644 --- a/libs/server/Resp/Objects/SortedSetCommands.cs +++ b/libs/server/Resp/Objects/SortedSetCommands.cs @@ -1550,11 +1550,16 @@ private unsafe bool SortedSetBlockingPop(RespCommand command) return AbortWithWrongNumberOfArguments(command.ToString()); } - if (!parseState.TryGetDouble(parseState.Count - 1, out var timeout) || (timeout < 0)) + if (!parseState.TryGetDouble(parseState.Count - 1, out var timeout)) { return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_NOT_VALID_FLOAT); } + if (timeout < 0) + { + return AbortWithErrorMessage(CmdStrings.RESP_ERR_TIMEOUT_IS_NEGATIVE); + } + var keysBytes = new byte[parseState.Count - 1][]; for (var i = 0; i < keysBytes.Length; i++) @@ -1564,23 +1569,29 @@ private unsafe bool SortedSetBlockingPop(RespCommand command) var result = storeWrapper.itemBroker.GetCollectionItemAsync(command, keysBytes, this, timeout).Result; - if (!result.Found) + switch (result.Status) { - WriteNull(); - } - else - { - while (!RespWriteUtils.TryWriteArrayLength(3, ref dcurr, dend)) - SendAndReset(); + case GarnetStatus.OK: + while (!RespWriteUtils.TryWriteArrayLength(3, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(result.Key, ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(result.Key, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(result.Item, ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(result.Item, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteDoubleBulkString(result.Score, ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteDoubleBulkString(result.Score, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.NOTFOUND: + default: + WriteNull(); + break; } return true; @@ -1668,30 +1679,37 @@ private unsafe bool SortedSetBlockingMPop() var result = storeWrapper.itemBroker.GetCollectionItemAsync(RespCommand.BZMPOP, keysBytes, this, timeout, cmdArgs).Result; - if (!result.Found) + switch (result.Status) { - WriteNull(); - return true; - } - - // Write array with 2 elements: key and array of member-score pairs - while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) - SendAndReset(); + case GarnetStatus.OK: + // Write array with 2 elements: key and array of member-score pairs + while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(result.Key, ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(result.Key, ref dcurr, dend)) + SendAndReset(); - while (!RespWriteUtils.TryWriteArrayLength(result.Items.Length, ref dcurr, dend)) - SendAndReset(); + while (!RespWriteUtils.TryWriteArrayLength(result.Items.Length, ref dcurr, dend)) + SendAndReset(); - for (var i = 0; i < result.Items.Length; ++i) - { - while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) - SendAndReset(); - while (!RespWriteUtils.TryWriteBulkString(result.Items[i], ref dcurr, dend)) - SendAndReset(); - while (!RespWriteUtils.TryWriteDoubleBulkString(result.Scores[i], ref dcurr, dend)) - SendAndReset(); + for (var i = 0; i < result.Items.Length; ++i) + { + while (!RespWriteUtils.TryWriteArrayLength(2, ref dcurr, dend)) + SendAndReset(); + while (!RespWriteUtils.TryWriteBulkString(result.Items[i], ref dcurr, dend)) + SendAndReset(); + while (!RespWriteUtils.TryWriteDoubleBulkString(result.Scores[i], ref dcurr, dend)) + SendAndReset(); + } + break; + case GarnetStatus.WRONGTYPE: + while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) + SendAndReset(); + break; + case GarnetStatus.NOTFOUND: + default: + WriteNull(); + break; } return true; diff --git a/test/Garnet.test/RespBlockingCollectionTests.cs b/test/Garnet.test/RespBlockingCollectionTests.cs index 7b7f193be27..52af96274c9 100644 --- a/test/Garnet.test/RespBlockingCollectionTests.cs +++ b/test/Garnet.test/RespBlockingCollectionTests.cs @@ -414,6 +414,11 @@ public void BlmpopBlockingWithCountTest() Task.WaitAll([blockingTask, pushingTask], TimeSpan.FromSeconds(10)); ClassicAssert.IsTrue(blockingTask.IsCompletedSuccessfully); ClassicAssert.IsTrue(pushingTask.IsCompletedSuccessfully); + + using var lightClientRequest = TestUtils.CreateRequest(); + var response = lightClientRequest.SendCommand($"EXISTS {key}"); + var expectedResponse = ":1\r\n"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); } [Test] @@ -470,6 +475,9 @@ public void BzmpopReturnTest() ClassicAssert.AreEqual(2, pop[1][1].Length); ClassicAssert.AreEqual("two", pop[1][1][0].ToString()); ClassicAssert.AreEqual(2, (int)(RedisValue)pop[1][1][1]); + + ClassicAssert.IsFalse(db.KeyExists("a")); + ClassicAssert.IsTrue(db.KeyExists("b")); } [Test] @@ -608,5 +616,87 @@ public void BzpopMinMaxTimeoutTest(string command) var expectedResponse = "$-1\r\n"; TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); } + + [Test] + public void PopWrongTypeTest() + { + using var lightClientRequest = TestUtils.CreateRequest(); + lightClientRequest.SendCommand("SET key 0"); + lightClientRequest.SendCommand("RPUSH list 0"); + lightClientRequest.SendCommand("ZADD set 0 first"); + + var response = lightClientRequest.SendCommand("BLMPOP 0 1 key RIGHT"); + var expectedResponse = "-WRONGTYPE Operation against a key holding the wrong kind of value.\r\n"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLMPOP 0 2 key list RIGHT"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLMPOP 0 1 set RIGHT"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BZMPOP 0 1 key MAX"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BZMPOP 0 2 key set MAX"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BZMPOP 0 1 list MAX"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BZPOPMIN key 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BZPOPMAX list 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLMOVE key foo RIGHT RIGHT 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLMOVE set foo RIGHT RIGHT 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLMOVE list set LEFT LEFT 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLMOVE list key LEFT LEFT 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BRPOPLPUSH key foo 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BRPOPLPUSH list key 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BLPOP key 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + response = lightClientRequest.SendCommand("BRPOP key 0"); + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + + var blockingLTask = Task.Run(() => + { + using var lcr = TestUtils.CreateRequest(); + var response = lcr.SendCommand("BLMPOP 0 2 list key RIGHT"); + var expectedResponse = "*2\r\n$4\r\nlist\r\n*1\r\n$1\r\n0\r\n"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + }); + + var blockingZTask = Task.Run(() => + { + using var lcr = TestUtils.CreateRequest(); + var response = lcr.SendCommand("BZMPOP 0 2 set key MAX"); + var expectedResponse = "*2\r\n$3\r\nset\r\n*1\r\n*2\r\n$5\r\nfirst\r\n$1\r\n0"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + }); + + Task.WaitAll([blockingLTask, blockingZTask], TimeSpan.FromSeconds(5)); + ClassicAssert.IsTrue(blockingLTask.IsCompletedSuccessfully); + ClassicAssert.IsTrue(blockingZTask.IsCompletedSuccessfully); + + response = lightClientRequest.SendCommand("EXISTS list"); + expectedResponse = ":0\r\n"; + TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response); + } } } \ No newline at end of file