DonalEvans commented on a change in pull request #7396:
URL: https://github.com/apache/geode/pull/7396#discussion_r817008483
##########
File path:
geode-for-redis/src/main/java/org/apache/geode/redis/internal/RedisConstants.java
##########
@@ -33,6 +33,7 @@
public static final String ERROR_UNKNOWN_COMMAND =
"unknown command `%s`, with args beginning with: %s";
public static final String ERROR_OUT_OF_RANGE = "The number provided is out
of range";
+ public static final String ERROR_INDEX_OUT_OF_RANGE = "index out of range";
Review comment:
After rebasing to pull in the changes from GEODE-10049, this string
should be "ERR index out of range"
##########
File path:
geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LSetExecutor.java
##########
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+
+package org.apache.geode.redis.internal.commands.executor.list;
+
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LSetExecutor implements CommandExecutor {
Review comment:
The order in which we handle error conditions in this class needs to be
tweaked a bit to match what Redis does. Currently the two tests below pass with
open source Redis but fail with this code:
```
@Test
public void
lset_returnsNotAnIntegerError_givenKeyExistsAndIndexIsNotAValidInteger() {
jedis.lpush(KEY, initialValue);
assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LSET,
KEY, "notAnInteger", "newElement")).hasMessage("ERR " + ERROR_NOT_INTEGER);
}
@Test
public void
lset_returnsNoSuchKeyError_givenKeyDoesNotExistAndIndexIsNotAValidInteger() {
assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LSET,
KEY, "notAnInteger", "newElement")).hasMessage("ERR " + ERROR_NO_SUCH_KEY);
}
```
We need to first check if the key exists, then check that the index is a
valid integer (using a try/catch around `Coder.bytesToLong()`), all while
holding the lock for the key. Changing the `executeCommand()` method to the
following should fix these issues:
```
public RedisResponse executeCommand(Command command, ExecutionHandlerContext
context) {
List<byte[]> commandElements = command.getProcessedCommand();
RedisKey key = command.getKey();
return context.listLockedExecute(key, false, list -> {
if (!list.exists()) {
return RedisResponse.error(ERROR_NO_SUCH_KEY);
}
int index;
try {
index = narrowLongToInt(bytesToLong(commandElements.get(2)));
} catch (NumberFormatException ex) {
return RedisResponse.error(ERROR_NOT_INTEGER);
}
Region<RedisKey, RedisData> region = context.getRegion();
byte[] value = commandElements.get(3);
list.lset(region, key, index, value);
return RedisResponse.ok();
});
}
```
##########
File path:
geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLSetIntegrationTest.java
##########
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+import static
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static
org.apache.geode.redis.internal.RedisConstants.ERROR_INDEX_OUT_OF_RANGE;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_NO_SUCH_KEY;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Random;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.RedisException;
+
+public abstract class AbstractLSetIntegrationTest implements
RedisIntegrationTest {
+ public static final String KEY = "key";
+ public static final String initialValue = "initialValue";
+ public static final String newValue = "newValue";
+ private JedisCluster jedis;
+
+ @Before
+ public void setUp() {
+ jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()),
REDIS_CLIENT_TIMEOUT);
+ }
+
+ @After
+ public void tearDown() {
+ flushAll();
+ jedis.close();
+ }
+
+ @Test
+ public void lset_errors_givenTooFewArguments() {
+ assertExactNumberOfArgs(jedis, Protocol.Command.LSET, 3);
+ }
+
+ @Test
+ public void lset_onKeyThatDoesNotExist_returnsError_doesNotCreateKey() {
+ assertThatThrownBy(() -> jedis.lset(KEY, 1, newValue))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessage("ERR " + ERROR_NO_SUCH_KEY);
+ assertThat(jedis.exists(KEY)).isFalse();
+ }
+
+ @Test
+ public void lset_errors_withIndexOutOfRange() {
+ jedis.lpush(KEY, initialValue);
+ assertThatThrownBy(() -> jedis.lset(KEY, 10, newValue))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessage("ERR " + ERROR_INDEX_OUT_OF_RANGE);
+
+ assertThatThrownBy(() -> jedis.lset(KEY, -10, newValue))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessage("ERR " + ERROR_INDEX_OUT_OF_RANGE);
+ }
+
+ @Test
+ public void lset_setsValue_givenValidPositiveIndex_withOneItemInList() {
+ jedis.lpush(KEY, initialValue);
+ assertThat(jedis.lset(KEY, 0, newValue)).isEqualTo("OK");
+ assertThat(jedis.lpop(KEY)).isEqualTo(newValue);
+ }
+
+ @Test
+ public void lset_setsValue_givenValidPositiveIndex_withMultipleItemsInList()
{
+ for (int i = 4; i >= 0; i--) {
+ jedis.lpush(KEY, initialValue + i);
+ }
+
+ assertThat(jedis.lset(KEY, 2, newValue)).isEqualTo("OK");
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "0");
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "1");
+ assertThat(jedis.lpop(KEY)).isEqualTo(newValue);
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "3");
+ }
+
+ @Test
+ public void lset_setsValue_givenValidNegativeIndex_withOneItemInList() {
+ jedis.lpush(KEY, initialValue);
+ assertThat(jedis.lset(KEY, -1, newValue)).isEqualTo("OK");
+ assertThat(jedis.lpop(KEY)).isEqualTo(newValue);
+ }
+
+ @Test
+ public void lset_setsValue_givenValidNegativeIndex_withMultipleItemsInList()
{
+ for (int i = 4; i >= 0; i--) {
+ jedis.lpush(KEY, initialValue + i);
+ }
+
+ assertThat(jedis.lset(KEY, -2, newValue)).isEqualTo("OK");
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "0");
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "1");
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "2");
+ assertThat(jedis.lpop(KEY)).isEqualTo(newValue);
+ assertThat(jedis.lpop(KEY)).isEqualTo(initialValue + "4");
+ }
+
+ @Test
+ public void lset_whenRunConcurrently_doesNotThrowRedisException() {
Review comment:
This test is not doing what you think it is, due to the way
`ConcurrentLoopingThreads` handles exceptions. Replacing the assertions in the
`catch` block with `assertThat(true).isFalse();` still results in the test
passing.
A better test of correct concurrent behaviour for LSET would be to have a
test that does an LSET concurrently with a multi-element LPUSH and confirms
that either we set the element before the LPUSH happened, or we set it after,
but not partway through:
```
@Test
public void lset_withConcurrentLpush_behavesCorrectly() {
String[] initialElements = {"1", "2", "3", "4", "5", "6"};
jedis.lpush(KEY, initialElements);
String[] elementsToAdd = {"a", "b", "c", "d", "e", "f"};
new ConcurrentLoopingThreads(1000,
i -> jedis.lpush(KEY, elementsToAdd),
i -> jedis.lset(KEY, 0, newValue)
).runWithAction(() -> {
assertThat(jedis.llen(KEY)).isEqualTo(initialElements.length +
elementsToAdd.length);
assertThat(jedis).satisfiesAnyOf(
// LPUSH was applied first
jedisClient -> {
assertThat(jedisClient.lindex(KEY, 0)).isEqualTo(newValue);
assertThat(jedisClient.lindex(KEY,
elementsToAdd.length)).isEqualTo(initialElements[initialElements.length - 1]);
},
// LSET was applied first
jedisClient -> {
assertThat(jedisClient.lindex(KEY,
0)).isEqualTo(elementsToAdd[elementsToAdd.length - 1]);
assertThat(jedisClient.lindex(KEY,
elementsToAdd.length)).isEqualTo(newValue);
}
);
// Reset the list to its original contents
jedis.del(KEY);
jedis.lpush(KEY, initialElements);
});
}
```
We could also have a test for LSET with concurrent LPOP on a single-element
list, to make sure that either LSET is applied first and we correctly set the
element in the list then pop it, or LPOP is applied first and we get an
ERROR_NO_SUCH_KEY response from LSET. If we get an ERROR_INDEX_OUT_OF_RANGE
response, we know that LSET attempted to set the value on a nonexistent list,
which shouldn't happen with correct locking:
```
@Test
public void lset_withConcurrentLpop_behavesCorrectly() {
jedis.lpush(KEY, initialValue);
AtomicReference<String> lpopResult = new AtomicReference<>();
AtomicReference<Throwable> lsetException = new AtomicReference<>();
new ConcurrentLoopingThreads(1000,
i -> lpopResult.set(jedis.lpop(KEY)),
i -> {
try {
jedis.lset(KEY, 0, newValue);
} catch (Exception ex) {
lsetException.set(ex);
}
}
).runWithAction(() -> {
assertThat(jedis.exists(KEY)).isFalse();
assertThat(lpopResult.get()).satisfiesAnyOf(
// LPOP was applied first
poppedValue -> {
assertThat(poppedValue).isEqualTo(initialValue);
assertThat(lsetException.get()).hasMessage("ERR " +
ERROR_NO_SUCH_KEY);
},
// LSET was applied first
poppedValue -> {
assertThat(poppedValue).isEqualTo(newValue);
assertThat(lsetException.get()).isNull();
}
);
// Reset the list to its original contents and clear the atomic
references
jedis.lpush(KEY, initialValue);
lpopResult.set(null);
lsetException.set(null);
});
}
```
##########
File path:
geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLSetIntegrationTest.java
##########
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+import static
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static
org.apache.geode.redis.internal.RedisConstants.ERROR_INDEX_OUT_OF_RANGE;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_NO_SUCH_KEY;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Random;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.RedisException;
+
+public abstract class AbstractLSetIntegrationTest implements
RedisIntegrationTest {
+ public static final String KEY = "key";
+ public static final String initialValue = "initialValue";
+ public static final String newValue = "newValue";
+ private JedisCluster jedis;
+
+ @Before
+ public void setUp() {
+ jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()),
REDIS_CLIENT_TIMEOUT);
+ }
+
+ @After
+ public void tearDown() {
+ flushAll();
+ jedis.close();
+ }
+
+ @Test
+ public void lset_errors_givenTooFewArguments() {
+ assertExactNumberOfArgs(jedis, Protocol.Command.LSET, 3);
+ }
+
+ @Test
+ public void lset_onKeyThatDoesNotExist_returnsError_doesNotCreateKey() {
+ assertThatThrownBy(() -> jedis.lset(KEY, 1, newValue))
+ .isInstanceOf(JedisDataException.class)
Review comment:
We shouldn't be asserting on the exception type returned here (or
elsewhere in this class), as that's controlled by Jedis, not our code. if the
Jedis client changes the exception they throw, this test will start failing
despite the behaviour of the geode-for-redis server being correct.
##########
File path:
geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
##########
@@ -593,6 +593,14 @@ public void testLlen() {
runCommandAndAssertHitsAndMisses(LIST_KEY, k -> jedis.llen(k));
}
+ @Test
+ public void testLset() {
+ runCommandAndAssertNoStatUpdates(LIST_KEY, k -> {
+ jedis.lpush(k, "value");
+ jedis.lset(k, 1, "newvalue");
+ });
+ }
Review comment:
This can be simplified to:
```
runCommandAndAssertNoStatUpdates(LIST_KEY, k -> jedis.lset(k, 0,
"newvalue"));
```
##########
File path:
geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
##########
@@ -50,4 +53,9 @@ public long lpush(List<byte[]> elementsToAdd,
Region<RedisKey, RedisData> region
public int llen() {
return 0;
}
+
+ @Override
+ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int
index, byte[] value) {
+ throw new RedisException(ERROR_NO_SUCH_KEY);
+ }
Review comment:
If the approach described for `LSetExecutor` is used, this method is no
longer needed.
##########
File path:
geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLSetIntegrationTest.java
##########
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+import static
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static
org.apache.geode.redis.internal.RedisConstants.ERROR_INDEX_OUT_OF_RANGE;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_NO_SUCH_KEY;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Random;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+import redis.clients.jedis.exceptions.JedisDataException;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.RedisException;
+
+public abstract class AbstractLSetIntegrationTest implements
RedisIntegrationTest {
Review comment:
In addition to the two tests described in the comment on `LSetExecutor`
could we also add a test for the behaviour when the key targeted by LSET is not
a list?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]