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: notifications-unsubscr...@geode.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


Reply via email to