You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@geode.apache.org by GitBox <gi...@apache.org> on 2022/01/12 22:14:26 UTC

[GitHub] [geode] ringles opened a new pull request #7261: Geode 9892 create infrastructure for redis lists

ringles opened a new pull request #7261:
URL: https://github.com/apache/geode/pull/7261


   Adds support for LPUSH, LPOP, LLEN to Geode-for-Redis


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r797984534



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();

Review comment:
       Rather than using `jedis.keys()` here, it would be better to use `jedis.exists(KEY)`. This would also remove the need to use a hashtag on the key.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopAndCrashesDUnitTest.java
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopAndCrashesDUnitTest {
+  public static final int NUM_VMS = 4;

Review comment:
       The default number of VMs started by `ClusterStartupRule` is 4, so it's not necessary to specify it here.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopAndCrashesDUnitTest.java
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopAndCrashesDUnitTest {

Review comment:
       I'm not sure that this test needs its own class. It should be possible to run all of the DUnit tests for LPOP from the same class as long as the `@Before` and `@After` methods are set up correctly. Specifically, if you're crashing VMs, the setup needs to be in a `@Before` rather than a `@BeforeClass` otherwise the VM won't get restarted between runs.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isEqualTo("e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();

Review comment:
       Rather than using `jedis.keys()` here, it would be better to use `jedis.exists(KEY)`. This would also remove the need to use a hashtag on the key.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    running.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private void lpushPerformAndVerify(int index, int minimumIterations, String hashtag) {

Review comment:
       Since we're focusing on testing LPOP in this class, it might be better to have LPUSH called just once, with a pre-populated list of elements. Also, that way, we can pass that same list of elements into the `lpopPerformAndVerify()` method and assert that we're popping them in the correct order rather than just asserting that we don't get duplicates.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    running.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private void lpushPerformAndVerify(int index, int minimumIterations, String hashtag) {
+    String key = makeListKeyWithHashtag(index, hashtag);
+    int iterationCount = 0;
+
+    while (iterationCount < minimumIterations) {
+      String elementString = makeElementString(key, iterationCount);
+      try {
+        jedis.lpush(key, elementString);
+      } catch (Exception ex) {
+        throw new RuntimeException("Exception performing LPUSH " + elementString, ex);
+      }
+      iterationCount += 1;
+    }
+
+    Long listLength = jedis.llen(key);
+    assertThat(listLength).isEqualTo(minimumIterations).withFailMessage("Initial list '"

Review comment:
       To use `withFailMessage()` correctly, it needs to be placed immediately after the `assertThat()` clause. However, since `withFailMessage()` overwrites the standard assertion error output entirely, it would be better to change this to `as()` with a description that only mentions the key that failed the assertion, since the lengths will be automatically reported by AssertJ.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,135 @@
+/*
+ * 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.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 java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPushDUnitTest {

Review comment:
       This class hasn't been updated to have meaningful DUnit tests added.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopAndCrashesDUnitTest.java
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopAndCrashesDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {

Review comment:
       This test is not really testing the distributed behaviour of LPOP, since we don't perform any LPOP commands until after we crash the server. A better test would be to pre-load server1 with a large list, pop all but the last element, then crash the server and assert that the data is consistent with what we expect. That way, if there is a problem with Delta propagation or some other distributed aspect of LPOP, we can see it.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    running.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private void lpushPerformAndVerify(int index, int minimumIterations, String hashtag) {
+    String key = makeListKeyWithHashtag(index, hashtag);
+    int iterationCount = 0;
+
+    while (iterationCount < minimumIterations) {
+      String elementString = makeElementString(key, iterationCount);
+      try {
+        jedis.lpush(key, elementString);
+      } catch (Exception ex) {
+        throw new RuntimeException("Exception performing LPUSH " + elementString, ex);
+      }
+      iterationCount += 1;
+    }
+
+    Long listLength = jedis.llen(key);
+    assertThat(listLength).isEqualTo(minimumIterations).withFailMessage("Initial list '"
+        + key + "' length " + listLength + " less than target " + minimumIterations);
+  }
+
+  private void lpopPerformAndVerify(int index, int minimumIterations, String hashtag,
+      AtomicBoolean isRunning) {
+    String key = makeListKeyWithHashtag(index, hashtag);
+    int iterationCount = 0;
+
+    List<String> elementList = new ArrayList<>();
+    while (iterationCount < minimumIterations && isRunning.get()) {

Review comment:
       Depending on timing, this loop could pop all the elements off the lists before we finish moving buckets around, in which case the test is just doing meaningless work. It might be better to change the way the test works so that, as long as we still have elements to pop, we keep moving buckets, but as soon as the list is empty, we stop.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }

Review comment:
       This loop is duplicated, so we can remove one of the copies.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isEqualTo("e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong lpopReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> lpopReference.set(jedis.llen(KEY)))

Review comment:
       This test is testing LLEN, not LPOP. This should be changed to `jedis.lpop(KEY)` and the assertions changed to verify that we either get back the last element in the original values or the last element in the added values. The final looped LPOP calls will also need to be replaced with a DEL followed by a LPUSH of the initial values, as otherwise the test will progressively muck up the original list contents and fail.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] dschneider-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
dschneider-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r797906937



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +

Review comment:
       I think you should definitely use JvmSizeUtils.memoryOverhead(byte[]) to compute the memory used by a a byte array; "element" in this case. If you look at the callers of this method you will see we already use it in RedisSet.MemberSet, RedisString, and SizeableBytes2ObjectOpenCustomHashMapWithCursor. If we have other code that does all this math to size a byte array, then yes, file a ticket to fix that. But don't let that stop you from cleaning up this new code now.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r800974904



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -64,71 +71,54 @@ public static void tearDown() {
   }
 
   @Test
-  public void shouldDistributeDataAmongCluster() {
+  public void lpush_ShouldPushMultipleElementsAtomically()

Review comment:
       This test is not testing distributed behaviour, as all the operations are being done to a list residing in a bucket that does not move during the test, and no validation of the contents of the secondary copy is done. This would be a decent integration test to add to `AbstractLPushIntegrationTest` (possibly refactored a little to use `ConcurrentLoopingThreads` rather than the `ExecutorServiceRule`, just to be consistent with other Integration tests), but in order to test distributed behaviour, we need to be either moving buckets directly or crashing servers to cause them to move, and doing some verification that the contents of the secondary bucket after failing over matches what we expect.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: GEODE-9892: create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r808125710



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }

Review comment:
       I think it's fine to validate destructively at this point, because we've stopped doing any operations on the keys and don't need to do any other validation once we confirm the contents of the lists. I agree that it would be good to update the tests once we have LINDEX, but there's nothing to stop us from adding that extra confirmation that the behaviour is correct right now.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788175177



##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -0,0 +1,499 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.redis.internal.data.AbstractRedisData.NO_EXPIRATION;
+import static org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisSet.setOpStoreResult;
+import static org.apache.geode.redis.internal.netty.Coder.stringToBytes;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import it.unimi.dsi.fastutil.bytes.ByteArrays;
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.serialization.ByteArrayDataInput;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.services.RegionProvider;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class RedisListTest {

Review comment:
       All updated with the new RedisElementList implementation.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }

Review comment:
       Removed.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1018001967


   This pull request **introduces 1 alert** when merging 7cb15a3b38ebbe7caf42e4c756ccf2488146138b into 3a36962edfcd30aa3afa3a50813c63bfc155f699 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-7d441839e712a643e1359e6aae82207371bfe3e4)
   
   **new alerts:**
   
   * 1 for Equals or hashCode on arrays


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793078249



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      elementPush(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  public synchronized byte[] elementRemove(int index) {
+    return elementList.remove(index);
+  }
+
+  public synchronized boolean elementRemove(byte[] element) {
+    return elementList.remove(element);
+  }
+
+  public synchronized void elementPush(byte[] element) {
+    elementList.addFirst(element);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elementList.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elementList.size() != elementList.size()) {
+      return false;
+    }
+    for (Object element : elementList) {
+      if (!redisList.elementList.contains((byte[]) element)) {

Review comment:
       Yup, fixed.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794943324



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
##########
@@ -0,0 +1,43 @@
+/*
+ * 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 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.commands.parameters.RedisParametersMismatchException;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LPopExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    // TODO: Size check needed until we implement 'count' arg (Redis 6.2+)
+    if (command.getProcessedCommand().size() > 2) {
+      throw new RedisParametersMismatchException(command.wrongNumberOfArgumentsErrorMessage());
+    }

Review comment:
       I've pulled it out. So long as we keep the LPOP argument count test Geode-specific we don't need this code, if we use exact() in RedisCommandType.

##########
File path: geode-for-redis/src/main/resources/org/apache/geode/redis/internal/services/sanctioned-geode-for-redis-serializables.txt
##########
@@ -3,6 +3,7 @@ org/apache/geode/redis/internal/commands/executor/BaseSetOptions$Exists,false
 org/apache/geode/redis/internal/commands/parameters/RedisParametersMismatchException,true,-643700717871858072
 org/apache/geode/redis/internal/data/RedisDataType,false,nullType:org/apache/geode/redis/internal/data/RedisData,toStringValue:java/lang/String
 org/apache/geode/redis/internal/data/RedisSortedSet$MemberAddResult,false
+org/apache/geode/redis/internal/data/collections/SizeableByteArrayList,false,memberOverhead:int

Review comment:
       Moved.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: GEODE-9892: create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r808060461



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtLeastNArgs;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_returnsWrongTypeError_shouldNotOverWriteExistingKey() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);

Review comment:
       Done.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,116 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicReference;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);

Review comment:
       Done.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,116 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicReference;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String key = KEY;
+
+    jedis.lpush(key, "e1");
+    assertThat(jedis.lpop(key)).isEqualTo("e1");
+    assertThat(jedis.lpop(key)).isNull();
+    assertThat(jedis.lpop(key)).isNull();
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void lpop_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "troix"};
+    String[] valuesToAdd = new String[] {"plum", "peach", "orange"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicReference<String> lpopReference = new AtomicReference<>();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> lpopReference.set(jedis.lpop(KEY)))
+            .runWithAction(() -> {
+              assertThat(lpopReference).satisfiesAnyOf(
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("orange"),
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("troix"),
+                  lpopResult -> assertThat(lpopReference.get()).isNull());

Review comment:
       Leftover from earlier iteration.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));

Review comment:
       Done.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), MINIMUM_ITERATIONS);
+    List<String> elementList2 = makeElementList(keys.get(1), MINIMUM_ITERATIONS);
+    List<String> elementList3 = makeElementList(keys.get(2), MINIMUM_ITERATIONS);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(keys.get(2), runningCount);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);

Review comment:
       There are a few of these in other tests. We'll need a ticket to clean them up.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), MINIMUM_ITERATIONS);
+    List<String> elementList2 = makeElementList(keys.get(1), MINIMUM_ITERATIONS);
+    List<String> elementList3 = makeElementList(keys.get(2), MINIMUM_ITERATIONS);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(keys.get(2), runningCount);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    runningCount.set(0);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+    return listHashtags;
+  }
+
+  private List<String> makeListKeys(List<String> listHashtags) {
+    List<String> keys = new ArrayList<>();
+    keys.add(makeListKeyWithHashtag(1, listHashtags.get(0)));
+    keys.add(makeListKeyWithHashtag(2, listHashtags.get(1)));
+    keys.add(makeListKeyWithHashtag(3, listHashtags.get(2)));
+    return keys;
+  }
+
+
+  private void lpushPerformAndVerify(String key, List<String> elementList) {
+    jedis.lpush(key, elementList.toArray(new String[] {}));
+
+    Long listLength = jedis.llen(key);
+    assertThat(listLength).isEqualTo(elementList.size())
+        .as("Initial list lengths not equal for key %s'", key);

Review comment:
       Moved.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), MINIMUM_ITERATIONS);
+    List<String> elementList2 = makeElementList(keys.get(1), MINIMUM_ITERATIONS);
+    List<String> elementList3 = makeElementList(keys.get(2), MINIMUM_ITERATIONS);

Review comment:
       Renamed. .isGreaterThanOrEqualTo() were from earlier iteration shuffling buckets when building lists, but that's not this test's concern.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()

Review comment:
       Renamed.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();

Review comment:
       Tweaked.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }

Review comment:
       Altered.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);

Review comment:
       Removed.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);

Review comment:
       Done.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }

Review comment:
       Without something like LINDEX that kind of validation can only be done destructively. There absolutely is a failure mode where only part of a list gets added if it's not handled atomically. We can add a ticket to update these with a LINDEX.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed
+  }
+
+  private void lpushPerformAndVerify(String key, List<String> elementList,
+      AtomicLong runningCount) {
+    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+      long listLength = jedis.llen(key);
+      long newLength = jedis.lpush(key, elementList.toArray(new String[] {}));
+      assertThat((newLength - listLength) % 3).as("LPUSH, list length %s not multiple of 3",
+          newLength).isEqualTo(0);
+    }
+    runningCount.decrementAndGet();
+  }
+
+  @Test
+  public void shouldDistributeElementsAcrossCluster()

Review comment:
       Renamed.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed
+  }
+
+  private void lpushPerformAndVerify(String key, List<String> elementList,
+      AtomicLong runningCount) {
+    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+      long listLength = jedis.llen(key);
+      long newLength = jedis.lpush(key, elementList.toArray(new String[] {}));
+      assertThat((newLength - listLength) % 3).as("LPUSH, list length %s not multiple of 3",
+          newLength).isEqualTo(0);
+    }
+    runningCount.decrementAndGet();
+  }
+
+  @Test
+  public void shouldDistributeElementsAcrossCluster()
+      throws ExecutionException, InterruptedException {
+    final int pusherCount = 6;
+    final int pushListSize = 3;
+    AtomicLong runningCount = new AtomicLong(pusherCount);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(pushListSize, "element1-");
+    List<String> elements2 = makeElementList(pushListSize, "element2-");
+    List<String> elements3 = makeElementList(pushListSize, "element2-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);

Review comment:
       All right, we'll need to fix it in StringsDUnitTest and RenameDUnitTest as well.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed
+  }
+
+  private void lpushPerformAndVerify(String key, List<String> elementList,
+      AtomicLong runningCount) {
+    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+      long listLength = jedis.llen(key);
+      long newLength = jedis.lpush(key, elementList.toArray(new String[] {}));
+      assertThat((newLength - listLength) % 3).as("LPUSH, list length %s not multiple of 3",
+          newLength).isEqualTo(0);
+    }
+    runningCount.decrementAndGet();
+  }
+
+  @Test
+  public void shouldDistributeElementsAcrossCluster()
+      throws ExecutionException, InterruptedException {
+    final int pusherCount = 6;
+    final int pushListSize = 3;
+    AtomicLong runningCount = new AtomicLong(pusherCount);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(pushListSize, "element1-");
+    List<String> elements2 = makeElementList(pushListSize, "element2-");
+    List<String> elements3 = makeElementList(pushListSize, "element2-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    clusterStartUp.crashVM(1); // kill primary server
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * pushListSize);
+      assertThat(length % 3).isEqualTo(0);
+    }

Review comment:
       Soon as we have LINDEX we can do that non-destructively.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1012514895


   This pull request **introduces 1 alert** when merging fab107dbfabe0a0030743f6ddc365604f2526620 into 2b032440eb9d0f3c68a94602289cc41435c68fad - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-d8c21078d9ade9f0d18a734fc8ee14c23017e3f9)
   
   **new alerts:**
   
   * 1 for Spurious Javadoc @param tags


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1012299203


   This pull request **introduces 1 alert** when merging e05937f602d8b81d25a69340a380e10698d0e96a into ac04dc4348c9231960b46cefca13da41c1bff97e - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-e9ab4778ae12c4d1cbc1361fa900f9e84610e55f)
   
   **new alerts:**
   
   * 1 for Spurious Javadoc @param tags


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788174280



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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 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.commands.parameters.RedisParametersMismatchException;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LPopExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    // Needed until we implement 'count' arg (Redis 6.2+)
+    if (command.getProcessedCommand().size() > 2) {
+      throw new RedisParametersMismatchException(command.wrongNumberOfArgumentsErrorMessage());
+    }

Review comment:
       As above, I think it better to keep the special case logic closer to the implementation. Hopefully it won't be there that long anyway.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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 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.commands.parameters.RedisParametersMismatchException;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LPopExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    // Needed until we implement 'count' arg (Redis 6.2+)
+    if (command.getProcessedCommand().size() > 2) {
+      throw new RedisParametersMismatchException(command.wrongNumberOfArgumentsErrorMessage());
+    }
+    byte[] popped = context.listLockedExecute(key, false,
+        list -> list.lpop(region, key));
+
+    if (popped == null) {
+      return RedisResponse.nil();
+    }
+
+    return RedisResponse.bulkString(popped);

Review comment:
       I know it failed at one point, but seems to pass now without the if block.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {

Review comment:
       Simplified

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }

Review comment:
       Cleared

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {

Review comment:
       Simplified

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.

Review comment:
       Names fixed in a couple places




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r801001140



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -64,71 +71,54 @@ public static void tearDown() {
   }
 
   @Test
-  public void shouldDistributeDataAmongCluster() {
+  public void lpush_ShouldPushMultipleElementsAtomically()

Review comment:
       Sorry for the premature review. I'll hold off on more comments until you give the thumbs up.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r798025823



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,180 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementPush(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementRemove(0);
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (byte[] element : elementList) {
+      DataSerializer.writeByteArray(element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    SizeableByteArrayList tempElementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      byte[] element = DataSerializer.readByteArray(in);
+      tempElementList.addLast(element);
+    }
+    elementList = tempElementList;

Review comment:
       Passes local tests, done.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r796135652



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,184 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementPush(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementRemove(0);
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (byte[] element : elementList) {
+      DataSerializer.writeByteArray(element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    SizeableByteArrayList tempElementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      byte[] element = DataSerializer.readByteArray(in);
+      tempElementList.addLast(element);
+    }
+    replaceElementList(tempElementList);
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  private synchronized void replaceElementList(SizeableByteArrayList newList) {
+    elementList = newList;
+  }

Review comment:
       If I yank it, the new and improved DUnit tests still pass, so I've removed it.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r799671091



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopAndCrashesDUnitTest.java
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopAndCrashesDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {

Review comment:
       Done.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    running.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private void lpushPerformAndVerify(int index, int minimumIterations, String hashtag) {

Review comment:
       Done.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }

Review comment:
       Yanked.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    running.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private void lpushPerformAndVerify(int index, int minimumIterations, String hashtag) {
+    String key = makeListKeyWithHashtag(index, hashtag);
+    int iterationCount = 0;
+
+    while (iterationCount < minimumIterations) {
+      String elementString = makeElementString(key, iterationCount);
+      try {
+        jedis.lpush(key, elementString);
+      } catch (Exception ex) {
+        throw new RuntimeException("Exception performing LPUSH " + elementString, ex);
+      }
+      iterationCount += 1;
+    }
+
+    Long listLength = jedis.llen(key);
+    assertThat(listLength).isEqualTo(minimumIterations).withFailMessage("Initial list '"

Review comment:
       Reworked.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,166 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int NUM_VMS = 4;
+  public static final int MINIMUM_ITERATIONS = 10000;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(NUM_VMS);
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean running = new AtomicBoolean(true);
+
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+
+    lpushPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0));
+    lpushPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1));
+    lpushPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2));
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(1, MINIMUM_ITERATIONS, listHashtags.get(0), running);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(2, MINIMUM_ITERATIONS, listHashtags.get(1), running);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(3, MINIMUM_ITERATIONS, listHashtags.get(2), running);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    for (int i = 0; i < 100 && running.get(); i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    }
+
+    running.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private void lpushPerformAndVerify(int index, int minimumIterations, String hashtag) {
+    String key = makeListKeyWithHashtag(index, hashtag);
+    int iterationCount = 0;
+
+    while (iterationCount < minimumIterations) {
+      String elementString = makeElementString(key, iterationCount);
+      try {
+        jedis.lpush(key, elementString);
+      } catch (Exception ex) {
+        throw new RuntimeException("Exception performing LPUSH " + elementString, ex);
+      }
+      iterationCount += 1;
+    }
+
+    Long listLength = jedis.llen(key);
+    assertThat(listLength).isEqualTo(minimumIterations).withFailMessage("Initial list '"
+        + key + "' length " + listLength + " less than target " + minimumIterations);
+  }
+
+  private void lpopPerformAndVerify(int index, int minimumIterations, String hashtag,
+      AtomicBoolean isRunning) {
+    String key = makeListKeyWithHashtag(index, hashtag);
+    int iterationCount = 0;
+
+    List<String> elementList = new ArrayList<>();
+    while (iterationCount < minimumIterations && isRunning.get()) {

Review comment:
       Used an AtomicLong to track running threads.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] jdeppe-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
jdeppe-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r785184404



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LLenExecutor.java
##########
@@ -0,0 +1,38 @@
+/*
+ * 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 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.ExecutionHandlerContext;
+
+public class LLenExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    int result = context.listLockedExecute(key, false,

Review comment:
       I think that his should be updating stats here. That thought leads me to point out that we're missing updates to `AbstractHitsMissesIntegrationTest` for lists.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788172720



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopArgumentCountIntegrationTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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 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 org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+// We only need this test because LPop has a different argument count between Redis 5.x
+// and Redis 6.2+. But the pipeline is testing against Redis 6.2.x. Once the "COUNT"
+// subcommand is implemented for LPOP, this test (and LPopArgumentCountIntegrationTest)
+// should be terminated with prejudice

Review comment:
       Had to make the 'jedis' in the Abstract parent class public for access in the integration tests; noted with TODO comments

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());

Review comment:
       Imported

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);

Review comment:
       Imported

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";

Review comment:
       Refactored

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";

Review comment:
       Refactored

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");

Review comment:
       Imported




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1018631158


   This pull request **introduces 1 alert** when merging f168ee00cd91841a3277d87bee28a78fd70b5103 into a2ed24199f59f89fb87deca81280e243115f18a9 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-98ff2d1473cae38bd5cd61ab3ba909e7371f4da7)
   
   **new alerts:**
   
   * 1 for Equals or hashCode on arrays


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r785180199



##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -0,0 +1,499 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.redis.internal.data.AbstractRedisData.NO_EXPIRATION;
+import static org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisSet.setOpStoreResult;
+import static org.apache.geode.redis.internal.netty.Coder.stringToBytes;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import it.unimi.dsi.fastutil.bytes.ByteArrays;
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.serialization.ByteArrayDataInput;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.services.RegionProvider;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class RedisListTest {

Review comment:
       I don't know how the changes got lost.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r785034333



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopArgumentCountIntegrationTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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 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 org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+// We only need this test because LPop has a different argument count between Redis 5.x
+// and Redis 6.2+. But the pipeline is testing against Redis 6.2.x. Once the "COUNT"
+// subcommand is implemented for LPOP, this test (and LPopArgumentCountIntegrationTest)
+// should be terminated with prejudice

Review comment:
       It shouldn't be necessary to have a separate test class for this, because of the way we structure our tests. We can instead have a test case in the `LpopIntegrationTest` (not the Abstract parent class) that validates the behaviour we expect from geode-for-redis without that test then being run against native Redis

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {

Review comment:
       Rather than having multiple commands tested in one test class, can we follow the convention of having a separate test class for each command? I realize that it's a little weird here, because it's not really possible to test LLEN and LPOP without also calling LPUSH, but it would make things neater and more consistent to have test classes separated by command.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int LIST_SIZE = 1000;
+  private static final int JEDIS_TIMEOUT =

Review comment:
       The `LOCAL_HOST` and `JEDIS_TIMEOUT` constants here should be removed and the equivalent constants from the `RedisClusterStartupRule` class used instead.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {

Review comment:
       Just for simplicity/readability, could this test use a String instead of byte arrays? The type of data used to call the Jedis command is irrelevant to testing server-side behaviour, since the format of the data sent to the server is the responsibility of the client rather than the server. Regardless of the type of the arguments we use here, they should be serialized into a format that's type-agnostic (byte arrays in the RESP2 protocol) and all handled by the server in the same way, so we can make the test a little more straightforward by just pushing and popping a String here.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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 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.commands.parameters.RedisParametersMismatchException;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LPopExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    // Needed until we implement 'count' arg (Redis 6.2+)
+    if (command.getProcessedCommand().size() > 2) {
+      throw new RedisParametersMismatchException(command.wrongNumberOfArgumentsErrorMessage());
+    }

Review comment:
       This is unnecessary if the parameter specification in `RedisCommandType` for LPOP is changed to `exact(2)`.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
##########
@@ -286,6 +289,12 @@
   ZUNIONSTORE(new ZUnionStoreExecutor(), SUPPORTED,
       new Parameter().min(4).flags(WRITE, DENYOOM, MOVABLEKEYS)),
 
+  /************** Lists *****************/
+
+  LLEN(new LLenExecutor(), SUPPORTED, new Parameter().exact(2).flags(READONLY, FAST)),
+  LPOP(new LPopExecutor(), SUPPORTED, new Parameter().min(2).flags(WRITE, FAST)),

Review comment:
       This should be `exact(2)`, since we do not accept LPOP with the count option. Since the `CommandIntegrationTest` will fail with this change (because it explicitly checks against the output from Redis 6.2.6) the `commandReturnsResultsMatchingNativeRedis()` test there should be modified to account for the difference, as we shouldn't be asserting that the info for the command matches behaviour that we haven't implemented.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!
+    for (int i = 0; i < size; ++i) {
+      elements.add(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  @VisibleForTesting
+  synchronized boolean appendElement(byte[] elementToAdd) {
+    return elements.add(elementToAdd);
+  }
+
+  @VisibleForTesting
+  synchronized boolean prependElement(byte[] elementToAdd) {
+    elements.add(0, elementToAdd);
+    return true;
+  }
+
+  @VisibleForTesting
+  synchronized boolean elementRemove(byte[] memberToRemove) {
+    return elements.remove(memberToRemove);
+  }

Review comment:
       The `@VisibleForTesting` annotation seems unnecessary, since these methods are never called from a test. As such, these methods should be made `private`.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableList.java
##########
@@ -0,0 +1,78 @@
+/*
+ * 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.data.collections;
+
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableList<K> extends LinkedList<K> implements Sizeable {
+  private static final long serialVersionUID = -8255284217236712060L;
+  private static final int SIZEABLE_LIST_OVERHEAD =
+      memoryOverhead(SizeableList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+
+  private int memberOverhead;
+
+  public SizeableList(Collection<K> collection) {
+    for (K element : collection) {
+      add(0, element);
+    }
+  }
+
+  public void add(int index, K k) {

Review comment:
       This method and the other add and remove methods should be overriding methods on `LinkedList`

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }
+
+  private RedisList.ElementList createTestElementList(int size) {
+    List<byte[]> initialElements = new ArrayList<>(size);
+    for (int i = 0; i < size; ++i) {
+      initialElements.add(new byte[] {(byte) i});
+    }
+    RedisList.ElementList list = new RedisList.ElementList(initialElements);
+    return list;
+  }
+
+  @Test
+  public void getSizeInBytesForEmptyElementList() {
+    RedisList.ElementList list = createTestElementList(0);
+    assertThat(list.getSizeInBytes()).isEqualTo(expectedSize(list));
+  }
+
+  @Test
+  public void getSizeInBytesIsAccurateForByteArrays() {
+    List<byte[]> initialElements = new ArrayList<>();
+    int initialNumberOfElements = 20;
+    int elementsToAdd = 100;
+    for (int i = 0; i < initialNumberOfElements; ++i) {
+      initialElements.add(new byte[] {(byte) i});
+    }
+    // Create a set with an initial size and confirm that it correctly reports its size
+    RedisList.ElementList list = new RedisList.ElementList(initialElements);
+    assertThat(list.getSizeInBytes()).isEqualTo(expectedSize(list));
+
+    // Add enough members to force a resize and assert that the size is correct after each add

Review comment:
       LinkedLists do not resize a backing array based on the number of elements, so this comment is misleading.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");

Review comment:
       Can this String be the `ERROR_WRONG_TYPE` constant instead?

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);

Review comment:
       Rather than using a hard-coded "localhost" here, please use the `BIND_ADDRESS` constant in `RedisClusterStartupRule`, similar to what other tests are doing.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());

Review comment:
       This constant is already defined in `RedisClusterStartupRule` so it's not necessary to redefine it here.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int LIST_SIZE = 1000;
+  private static final int JEDIS_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private static JedisCluster jedis;
+
+  private static MemberVM locator;
+  private static MemberVM server1;
+  private static MemberVM server2;
+  private static MemberVM server3;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    server1 = clusterStartUp.startRedisVM(1, locator.getPort());
+    server2 = clusterStartUp.startRedisVM(2, locator.getPort());
+    server3 = clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(LOCAL_HOST, redisServerPort), JEDIS_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+
+    server1.stop();
+    server2.stop();
+    server3.stop();

Review comment:
       It's not necessary to manually stop the servers, as the `RedisClusterStartupRule` takes care of that for you.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,146 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPushDUnitTest {

Review comment:
       See comments on `LPopDUnitTest`

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {

Review comment:
       The signature of this method doesn't need to take any arguments. It can be just `public int llen() {`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";

Review comment:
       It's not necessary to create an array here, the call to lpush can just be:
   ```
   jedis.lpush(KEY, "value")
   ```

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {

Review comment:
       It shouldn't be possible for `elementsAdded` to be 0, since calling LPUSH without any elements will result in an exception in the executor before we ever get here.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {

Review comment:
       None of the tests in this class are testing distributed behaviour, as the keys in them remain on the same server throughout the test and are independent in the test where there are multiple keys. The behaviour covered in this test is already covered in full in the LPOP integration tests.
   
   In order to be testing distributed behaviour, the test would need to move buckets and/or crash servers, but given the issues with flakiness we've seen in numerous other Redis-related DUnit tests, it might be more trouble than it's worth to try to add such tests.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {
+    byte[] blob = new byte[256];
+    for (int i = 0; i < 256; i++) {
+      blob[i] = (byte) i;
+    }
+
+    jedis.lpush(KEY.getBytes(), blob, blob);
+
+    byte[] result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+    result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+  }
+
+  // not checking LPOP argument count here, see AbstractLPopArgumentCountIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining("WRONGTYPE");
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isIn("e1", "e2");

Review comment:
       This test should be asserting that the returned element is equal to "e2" rather than one of "e1" or "e2", since we expect that LPOP returns the leftmost element in the list.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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 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.commands.parameters.RedisParametersMismatchException;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LPopExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    // Needed until we implement 'count' arg (Redis 6.2+)
+    if (command.getProcessedCommand().size() > 2) {
+      throw new RedisParametersMismatchException(command.wrongNumberOfArgumentsErrorMessage());
+    }
+    byte[] popped = context.listLockedExecute(key, false,
+        list -> list.lpop(region, key));
+
+    if (popped == null) {
+      return RedisResponse.nil();
+    }
+
+    return RedisResponse.bulkString(popped);

Review comment:
       This can be simplified to just `return RedisResponse.bulkString(popped);`, since the `bulkString()` method handles the case that the String is null.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {
+    byte[] blob = new byte[256];
+    for (int i = 0; i < 256; i++) {
+      blob[i] = (byte) i;
+    }
+
+    jedis.lpush(KEY.getBytes(), blob, blob);
+
+    byte[] result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+    result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+  }
+
+  // not checking LPOP argument count here, see AbstractLPopArgumentCountIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining("WRONGTYPE");

Review comment:
       Could this error string be the `ERROR_WRONG_TYPE` constant instead please?

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int LIST_SIZE = 1000;
+  private static final int JEDIS_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private static JedisCluster jedis;
+
+  private static MemberVM locator;

Review comment:
       `locator` can be a local variable in the `classSetup()` method.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";

Review comment:
       It's not necessary to create an array here, the call to lpush can just be:
   ```
   jedis.lpush(KEY, "value")
   ```

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {

Review comment:
       It's not possible for the popped element here to be null, because we've already established that `elements` is not empty.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!

Review comment:
       This TODO should be removed and the zero-arg constructor used.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }

Review comment:
       It's not possible for `originalSize` to be 0 here, since if the list becomes empty, it is removed from the region.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.

Review comment:
       This comment refers to "members" rather than "elements". Also, despite what this comment says, the `lpop()`method (which modifies `elements`) is not synchronized, and therefore not threadsafe with `toData()`.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableList.java
##########
@@ -0,0 +1,78 @@
+/*
+ * 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.data.collections;
+
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableList<K> extends LinkedList<K> implements Sizeable {
+  private static final long serialVersionUID = -8255284217236712060L;
+  private static final int SIZEABLE_LIST_OVERHEAD =
+      memoryOverhead(SizeableList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+
+  private int memberOverhead;
+
+  public SizeableList(Collection<K> collection) {
+    for (K element : collection) {
+      add(0, element);
+    }

Review comment:
       Calling `add()` here is potentially unsafe, as the `add()` method is public and could potentially be overridden, causing unspecified behaviour in the constructor. I'm not sure what the best solution here is, since calling the super constructor would require overriding `addAll(Collection<? extends E> collection)` to handle the sizing correctly, but that would mean iterating through the collection twice, which isn't ideal.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!
+    for (int i = 0; i < size; ++i) {
+      elements.add(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  @VisibleForTesting
+  synchronized boolean appendElement(byte[] elementToAdd) {
+    return elements.add(elementToAdd);
+  }
+
+  @VisibleForTesting
+  synchronized boolean prependElement(byte[] elementToAdd) {
+    elements.add(0, elementToAdd);
+    return true;
+  }
+
+  @VisibleForTesting
+  synchronized boolean elementRemove(byte[] memberToRemove) {
+    return elements.remove(memberToRemove);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elements.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elements.size() != elements.size()) {
+      return false;
+    }
+    for (byte[] element : elements) {
+      if (!redisList.elements.contains(element)) {
+        return false;
+      }

Review comment:
       This equals method is incorrect, as lists require not just that the contents of the collections are the same, but that they're in the same order too. This should iterate both lists and check that the element at each index is the same (using `Arrays.equals()`) in both.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!
+    for (int i = 0; i < size; ++i) {
+      elements.add(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  @VisibleForTesting
+  synchronized boolean appendElement(byte[] elementToAdd) {
+    return elements.add(elementToAdd);
+  }
+
+  @VisibleForTesting
+  synchronized boolean prependElement(byte[] elementToAdd) {

Review comment:
       The return value of this method is never used, and it always returns true, so it should probably just be a `void` method.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();

Review comment:
       The `RemoveByteArrays` Delta class is not sufficient for use with lists, because it assumes that only one instance of each byte array is possible in the collection (since it's used with Hash, Set and SortedSet, which do not allow duplicate entries). A new `DeltaInfo` class will be required which removes list elements by index, starting from the largest index so that the indexes of subsequent elements to remove aren't changed by removing other elements first.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!
+    for (int i = 0; i < size; ++i) {
+      elements.add(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  @VisibleForTesting
+  synchronized boolean appendElement(byte[] elementToAdd) {
+    return elements.add(elementToAdd);
+  }
+
+  @VisibleForTesting
+  synchronized boolean prependElement(byte[] elementToAdd) {
+    elements.add(0, elementToAdd);
+    return true;
+  }
+
+  @VisibleForTesting
+  synchronized boolean elementRemove(byte[] memberToRemove) {
+    return elements.remove(memberToRemove);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elements.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elements.size() != elements.size()) {
+      return false;
+    }
+    for (byte[] element : elements) {
+      if (!redisList.elements.contains(element)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), elements);
+  }
+
+  @Override
+  public String toString() {
+    return "RedisList{" + super.toString() + ", " + "size=" + elements.size() + '}';
+  }
+
+  @Override
+  public KnownVersion[] getSerializationVersions() {
+    return null;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return REDIS_LIST_OVERHEAD + elements.getSizeInBytes();
+  }
+
+  public static class ElementList extends SizeableList<byte[]> {
+    public ElementList(Collection<byte[]> initialElements) {
+      super(initialElements);
+    }
+
+    public ElementList() {
+      super(Collections.emptyList());

Review comment:
       Rather than calling the super constructor with an empty collection, it would be neater to add a zero-arg constructor to the super class and call that.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableList.java
##########
@@ -0,0 +1,78 @@
+/*
+ * 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.data.collections;
+
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableList<K> extends LinkedList<K> implements Sizeable {

Review comment:
       Could the generic type for this class be named `E` rather than `K` please? `K` is usually used for keys in maps, whereas `E` is for elements in lists and sets.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }
+
+  private RedisList.ElementList createTestElementList(int size) {

Review comment:
       This method is only used once, so it should probably just be inlined.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -0,0 +1,499 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.redis.internal.data.AbstractRedisData.NO_EXPIRATION;
+import static org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisSet.setOpStoreResult;
+import static org.apache.geode.redis.internal.netty.Coder.stringToBytes;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import it.unimi.dsi.fastutil.bytes.ByteArrays;
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.serialization.ByteArrayDataInput;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.services.RegionProvider;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class RedisListTest {

Review comment:
       Every test in this class is testing `RedisSet` rather than `RedisList`. When writing tests, especially unit tests, it's best not to copy/paste code.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableList.java
##########
@@ -0,0 +1,78 @@
+/*
+ * 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.data.collections;
+
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableList<K> extends LinkedList<K> implements Sizeable {
+  private static final long serialVersionUID = -8255284217236712060L;
+  private static final int SIZEABLE_LIST_OVERHEAD =
+      memoryOverhead(SizeableList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+
+  private int memberOverhead;
+
+  public SizeableList(Collection<K> collection) {
+    for (K element : collection) {
+      add(0, element);
+    }
+  }
+
+  public void add(int index, K k) {
+    super.add(index, k);
+    memberOverhead += sizeElement(k) + NODE_OVERHEAD;
+  }
+
+  public boolean add(K k) {
+    boolean added = super.add(k);
+    if (added) {
+      memberOverhead += sizeElement(k) + NODE_OVERHEAD;
+    }
+    return added;
+  }
+
+  @SuppressWarnings("unchecked")

Review comment:
       This suppression is unnecessary.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }

Review comment:
       Extracting this to its own method doesn't really gain us much in terms of simplification, so I'd prefer to inline it and remove this method.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }
+
+  private RedisList.ElementList createTestElementList(int size) {
+    List<byte[]> initialElements = new ArrayList<>(size);
+    for (int i = 0; i < size; ++i) {
+      initialElements.add(new byte[] {(byte) i});
+    }
+    RedisList.ElementList list = new RedisList.ElementList(initialElements);
+    return list;
+  }
+
+  @Test
+  public void getSizeInBytesForEmptyElementList() {
+    RedisList.ElementList list = createTestElementList(0);

Review comment:
       This can be simplified to just `RedisList.ElementList list = new RedisList.ElementList();`




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] dschneider-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
dschneider-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r797916793



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,180 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementPush(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementRemove(0);
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (byte[] element : elementList) {
+      DataSerializer.writeByteArray(element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    SizeableByteArrayList tempElementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      byte[] element = DataSerializer.readByteArray(in);
+      tempElementList.addLast(element);
+    }
+    elementList = tempElementList;

Review comment:
       I might be missing something but it looks to me like if you changed this method to not assign "elementList" then the "elementList" field could be final. Making it final is worth doing. fromData is called an an instance that was just created with the default constructor which sets "elementList" to a new empty list. So couldn't this method just add to "elementList" instead of a temp list?
   And then change "elementList" to be "final".

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        iterator.remove();
+        memberOverhead -= calculateByteArrayOverhead(element);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int primeNumber = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();

Review comment:
       I'm not sure why an iterator is used here. Could you instead just do `for (byte[] element: this)`?

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        iterator.remove();
+        memberOverhead -= calculateByteArrayOverhead(element);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int primeNumber = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      hashCode = hashCode * primeNumber + Arrays.hashCode(iterator.next());
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof SizeableByteArrayList)) {
+      return false;
+    }
+    SizeableByteArrayList sizeableByteArrayList = (SizeableByteArrayList) o;
+    if (sizeableByteArrayList.size() != this.size()) {
+      return false;
+    }
+    for (int i = 0; i < sizeableByteArrayList.size(); i++) {

Review comment:
       given that these are linked lists it seems wrong keep calling get(i) which is going to need to scan the list each time because random access is not supported. I think it would be better to create two iterators, one for each list, and then loop around advancing each iterator on each iteration.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        iterator.remove();
+        memberOverhead -= calculateByteArrayOverhead(element);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int primeNumber = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      hashCode = hashCode * primeNumber + Arrays.hashCode(iterator.next());

Review comment:
       could this be `hashCode *= primeNumber + ...`?




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r797986488



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        iterator.remove();
+        memberOverhead -= calculateByteArrayOverhead(element);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int primeNumber = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      hashCode = hashCode * primeNumber + Arrays.hashCode(iterator.next());
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof SizeableByteArrayList)) {
+      return false;
+    }
+    SizeableByteArrayList sizeableByteArrayList = (SizeableByteArrayList) o;
+    if (sizeableByteArrayList.size() != this.size()) {
+      return false;
+    }
+    for (int i = 0; i < sizeableByteArrayList.size(); i++) {

Review comment:
       O(N^2) to O(N).




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793078904



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);

Review comment:
       Using synchronized methods now.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();

Review comment:
       Fixed.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r800979728



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -64,71 +71,54 @@ public static void tearDown() {
   }
 
   @Test
-  public void shouldDistributeDataAmongCluster() {
+  public void lpush_ShouldPushMultipleElementsAtomically()

Review comment:
       Yeah, this is a draft, I didn't claim it was done yet. But since I have tests passing locally that are failing in CI, I wanted to make sure this was also running in CI.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r796093309



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,139 @@
+/*
+ * 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.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 java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+  private static final String KEY_BASE = "key";
+  private static final int NUM_LISTS = 100;
+  private static final String ELEMENT_BASE = "element-";
+  public static final int NUM_VMS = 4;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final int LIST_SIZE = 100;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash()
+      throws Exception {
+    List<String> elementList1 = makeElementList(0, LIST_SIZE / 2, ELEMENT_BASE);
+    List<String> elementList2 = makeElementList(LIST_SIZE / 2, LIST_SIZE, ELEMENT_BASE);
+    List<String> allElements = makeElementList(0, LIST_SIZE, ELEMENT_BASE);
+
+    new ConcurrentLoopingThreads(NUM_LISTS / 2,
+        (i) -> jedis.lpush(KEY_BASE + i, elementList1.toArray(new String[] {})),
+        (i) -> jedis.lpush(KEY_BASE + i, elementList2.toArray(new String[] {})),
+        (i) -> jedis.lpush(KEY_BASE + (i + NUM_LISTS / 2), elementList1.toArray(new String[] {})),
+        (i) -> jedis.lpush(KEY_BASE + (i + NUM_LISTS / 2), elementList2.toArray(new String[] {})))
+            .runInLockstep();
+
+    clusterStartUp.crashVM(NUM_VMS - 1);
+
+    confirmAllDataIsPresent(allElements);
+  }

Review comment:
       Have a couple tests along those lines now, instead. Had to use three servers for both; in the first one, if you crash the second, it fails because it can't ensure required redundancy.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793078412



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();

Review comment:
       Reworked to avoid that.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] dschneider-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
dschneider-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r792071643



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();

Review comment:
       elementList.removeFirst makes a change to "elementList" without syncing on this so I think it is not safe with toData

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.data.delta;
+
+import static org.apache.geode.DataSerializer.readInteger;
+import static org.apache.geode.internal.InternalDataSerializer.readArrayLength;
+import static org.apache.geode.redis.internal.data.delta.DeltaType.REMOVE_ELEMENTS_BY_INDEX;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.internal.InternalDataSerializer;
+import org.apache.geode.redis.internal.data.AbstractRedisData;
+
+public class RemoveElementsByIndex implements DeltaInfo {
+  private final List<Integer> indexes;
+
+  public void add(int index) {
+    indexes.add(index);
+  }
+
+  public RemoveElementsByIndex() {
+    this.indexes = new ArrayList<>();
+  }
+
+  public void serializeTo(DataOutput out) throws IOException {
+    DataSerializer.writeEnum(REMOVE_ELEMENTS_BY_INDEX, out);
+    InternalDataSerializer.writeArrayLength(indexes.size(), out);
+    for (int index : indexes) {
+      DataSerializer.writeInteger(index, out);

Review comment:
       writePrimitiveInteger is a little better (it avoids an auto-box)

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);

Review comment:
       this for loop makes changes to "elementList" without syncing on "this" so I think it is not safe with toData

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      elementPush(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  public synchronized byte[] elementRemove(int index) {
+    return elementList.remove(index);
+  }
+
+  public synchronized boolean elementRemove(byte[] element) {
+    return elementList.remove(element);
+  }
+
+  public synchronized void elementPush(byte[] element) {
+    elementList.addFirst(element);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elementList.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elementList.size() != elementList.size()) {
+      return false;
+    }
+    for (Object element : elementList) {
+      if (!redisList.elementList.contains((byte[]) element)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), elementList);

Review comment:
       I think this will do the wrong thing for the elements. hashCode on a byte[] is based on it's identity instead of its content.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      elementPush(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  public synchronized byte[] elementRemove(int index) {
+    return elementList.remove(index);
+  }
+
+  public synchronized boolean elementRemove(byte[] element) {
+    return elementList.remove(element);
+  }
+
+  public synchronized void elementPush(byte[] element) {
+    elementList.addFirst(element);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elementList.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elementList.size() != elementList.size()) {
+      return false;
+    }
+    for (Object element : elementList) {
+      if (!redisList.elementList.contains((byte[]) element)) {

Review comment:
       contains seems wrong here. Shouldn't you iterate both lists and compare each element?

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {

Review comment:
       given that "elementList" is a SizeableByteArrayList shouldn't you be able to iterate it as a sequence of "byte[]" instead of Object?

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();

Review comment:
       better would be to create the list with a local variable and then after the for loop to set the "elementList" inst var. Just so you don't need to worry about concurrency issues.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      elementPush(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  public synchronized byte[] elementRemove(int index) {
+    return elementList.remove(index);
+  }
+
+  public synchronized boolean elementRemove(byte[] element) {
+    return elementList.remove(element);
+  }
+
+  public synchronized void elementPush(byte[] element) {
+    elementList.addFirst(element);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elementList.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elementList.size() != elementList.size()) {
+      return false;
+    }
+    for (Object element : elementList) {

Review comment:
       can this be "byte[]" instead of Object?

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.data.delta;
+
+import static org.apache.geode.DataSerializer.readInteger;
+import static org.apache.geode.internal.InternalDataSerializer.readArrayLength;
+import static org.apache.geode.redis.internal.data.delta.DeltaType.REMOVE_ELEMENTS_BY_INDEX;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.internal.InternalDataSerializer;
+import org.apache.geode.redis.internal.data.AbstractRedisData;
+
+public class RemoveElementsByIndex implements DeltaInfo {
+  private final List<Integer> indexes;
+
+  public void add(int index) {
+    indexes.add(index);
+  }
+
+  public RemoveElementsByIndex() {
+    this.indexes = new ArrayList<>();
+  }
+
+  public void serializeTo(DataOutput out) throws IOException {
+    DataSerializer.writeEnum(REMOVE_ELEMENTS_BY_INDEX, out);
+    InternalDataSerializer.writeArrayLength(indexes.size(), out);
+    for (int index : indexes) {
+      DataSerializer.writeInteger(index, out);
+    }
+  }
+
+  public static void deserializeFrom(DataInput in, AbstractRedisData redisData) throws IOException {
+    int size = readArrayLength(in);
+    List<Integer> indexes = new ArrayList<>();
+    while (size > 0) {
+      indexes.add(readInteger(in));

Review comment:
       If you changed applyRemoveElementsByIndex(List<Integer>) to applyRemoveElementByIndex(int) you could use readPrimitiveInteger here and would not need to allocate the ArrayList

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +

Review comment:
       would it be better for this to use JvmSizeUtils?
   If it did I think it would just be `return NODE_OVERHEAD + JvmSizeUtils.memoryOverhead(element)`
   This would also allow you to get rid of BYTE_ARRAY_BASE_OVERHEAD




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r798025495



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        iterator.remove();
+        memberOverhead -= calculateByteArrayOverhead(element);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int primeNumber = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();

Review comment:
       Used to using iterators in the regular code, but sure...




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r797979413



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        iterator.remove();
+        memberOverhead -= calculateByteArrayOverhead(element);
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int primeNumber = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      hashCode = hashCode * primeNumber + Arrays.hashCode(iterator.next());

Review comment:
       Doesn't pass RedisListTest.hashcode_returnsDifferentValue_forDifferentLists() unless it's the other way. If we really want to use 'times-equals' it'll need to be two clauses.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788174801



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!

Review comment:
       Done




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788176325



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LLenExecutor.java
##########
@@ -0,0 +1,38 @@
+/*
+ * 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 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.ExecutionHandlerContext;
+
+public class LLenExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    int result = context.listLockedExecute(key, false,

Review comment:
       Corrected

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/GeodeRedisService.java
##########
@@ -60,6 +60,7 @@ public void register(DataSerializableFixedIdRegistrar registrar) {
     registrar.register(DataSerializableFixedID.REDIS_KEY, RedisKey.class);
     registrar.register(DataSerializableFixedID.PUBLISH_REQUEST,
         Publisher.PublishRequest.class);
+    registrar.register(DataSerializableFixedID.REDIS_LIST_ID, RedisSet.class);

Review comment:
       Fixed




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788180489



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
##########
@@ -286,6 +289,12 @@
   ZUNIONSTORE(new ZUnionStoreExecutor(), SUPPORTED,
       new Parameter().min(4).flags(WRITE, DENYOOM, MOVABLEKEYS)),
 
+  /************** Lists *****************/
+
+  LLEN(new LLenExecutor(), SUPPORTED, new Parameter().exact(2).flags(READONLY, FAST)),
+  LPOP(new LPopExecutor(), SUPPORTED, new Parameter().min(2).flags(WRITE, FAST)),

Review comment:
       This will result in incorrect information being returned by the COMMAND command though, since that parses the contents of `RedisCommandType` to generate its output. We only accept exactly 2 arguments for the LPOP command, so that's what should be returned by the COMMAND command, and hence what should be set here.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1016935740


   This pull request **introduces 1 alert** when merging 10c5a42cd98ddc8706230b28a3dbb33917739d80 into 7e8632137ec93fb643efb7f4ac88fbda328c18bf - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-058822409abc2d8c5a8d956c5d028c0b8bd1283a)
   
   **new alerts:**
   
   * 1 for Equals or hashCode on arrays


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r799670990



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopAndCrashesDUnitTest.java
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopAndCrashesDUnitTest {
+  public static final int NUM_VMS = 4;

Review comment:
       Yanked from a couple places.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794943721



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {

Review comment:
       Yup.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;

Review comment:
       Without it, we get "JedisCluster only supports KEYS commands with patterns containing hash-tags ( curly-brackets enclosed strings )". Clarified in constant name, also no longer ALL_CAPS to fit with coding style.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    assertThat(jedis.keys(KEY_WITH_TAG)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    jedis.lpop(KEY_WITH_TAG);
+    jedis.lpop(KEY_WITH_TAG);

Review comment:
       Suspenders added to belt.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {

Review comment:
       Simplified.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {

Review comment:
       We should file a ticket to pull similar tests from Hashes and Sets, too.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {
+    byte[] blob = new byte[256];
+    for (int i = 0; i < 256; i++) {
+      blob[i] = (byte) i;
+    }
+
+    jedis.lpush(KEY.getBytes(), blob, blob);
+
+    byte[] result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+    result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+  }
+
+  @Test
+  public void lpush_returnsUpdatedListLength() {
+    Long result = jedis.lpush(KEY, "e1");
+    assertThat(result).isEqualTo(jedis.llen(KEY));
+
+    result = jedis.lpush(KEY, "e2");
+    assertThat(result).isEqualTo(jedis.llen(KEY));
+
+    result = jedis.lpush(KEY, "e3");
+    assertThat(result).isEqualTo(jedis.llen(KEY));

Review comment:
       Only dependent on LPUSH now.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.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 java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPushDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final int LIST_SIZE = 1000;
+  private static JedisCluster jedis;
+
+  private static MemberVM server1;
+  private static MemberVM server2;
+  private static MemberVM server3;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    server1 = clusterStartUp.startRedisVM(1, locator.getPort());
+    server2 = clusterStartUp.startRedisVM(2, locator.getPort());
+    server3 = clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+
+    server1.stop();
+    server2.stop();
+    server3.stop();

Review comment:
       Yeah, gonna need a ticket for about five other places in the code, too.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794952463



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;

Review comment:
       Thinking about it, it would be easiest to just make the `KEY` constant have a tag on it, so then we don't need to create a different key for some test cases but not others.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788173018



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {
+    byte[] blob = new byte[256];
+    for (int i = 0; i < 256; i++) {
+      blob[i] = (byte) i;
+    }
+
+    jedis.lpush(KEY.getBytes(), blob, blob);
+
+    byte[] result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+    result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+  }
+
+  // not checking LPOP argument count here, see AbstractLPopArgumentCountIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining("WRONGTYPE");

Review comment:
       Refactored

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {
+
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  private static final int REDIS_CLIENT_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort("localhost", getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  @Test
+  public void lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining("Operation against a key holding the wrong kind of value");
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String[] elementValue = new String[1];
+    elementValue[0] = "set value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {
+    byte[] blob = new byte[256];
+    for (int i = 0; i < 256; i++) {
+      blob[i] = (byte) i;
+    }
+
+    jedis.lpush(KEY.getBytes(), blob, blob);
+
+    byte[] result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+    result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+  }
+
+  // not checking LPOP argument count here, see AbstractLPopArgumentCountIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining("WRONGTYPE");
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isIn("e1", "e2");

Review comment:
       Tweaked

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int LIST_SIZE = 1000;
+  private static final int JEDIS_TIMEOUT =

Review comment:
       Imported

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int LIST_SIZE = 1000;
+  private static final int JEDIS_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private static JedisCluster jedis;
+
+  private static MemberVM locator;

Review comment:
       Done

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final String LOCAL_HOST = "127.0.0.1";
+  private static final int LIST_SIZE = 1000;
+  private static final int JEDIS_TIMEOUT =
+      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  private static JedisCluster jedis;
+
+  private static MemberVM locator;
+  private static MemberVM server1;
+  private static MemberVM server2;
+  private static MemberVM server3;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    server1 = clusterStartUp.startRedisVM(1, locator.getPort());
+    server2 = clusterStartUp.startRedisVM(2, locator.getPort());
+    server3 = clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(LOCAL_HOST, redisServerPort), JEDIS_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+
+    server1.stop();
+    server2.stop();
+    server3.stop();

Review comment:
       Removed

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {

Review comment:
       Stripped.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
##########
@@ -286,6 +289,12 @@
   ZUNIONSTORE(new ZUnionStoreExecutor(), SUPPORTED,
       new Parameter().min(4).flags(WRITE, DENYOOM, MOVABLEKEYS)),
 
+  /************** Lists *****************/
+
+  LLEN(new LLenExecutor(), SUPPORTED, new Parameter().exact(2).flags(READONLY, FAST)),
+  LPOP(new LPopExecutor(), SUPPORTED, new Parameter().min(2).flags(WRITE, FAST)),

Review comment:
       Currently, I have the LPOP executor check for more than two args and throw the appropriate error there. (Marked with a TODO comment, but it'll be found pretty fast as soon as we implement Redis 6.2+ semantics.) I think it better to keep the 'special case' logic where LPOP is implemented and tested.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788175665



##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }
+
+  private RedisList.ElementList createTestElementList(int size) {

Review comment:
       This class is gone now.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1018704466


   This pull request **introduces 1 alert** when merging 8438b7045f37aeb8ac159042abb3ddb3ab2d9fe3 into a2ed24199f59f89fb87deca81280e243115f18a9 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-262a0d33d522175fb4ed730776beeeb8e18a43d0)
   
   **new alerts:**
   
   * 1 for Equals or hashCode on arrays


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793081039



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +

Review comment:
       All of the other Redis data types use `JvmSizeUtils.memoryOverhead()`, they just access it via a static import rather than qualified access:
   ```
   protected static final int REDIS_HASH_OVERHEAD = memoryOverhead(RedisHash.class);
   ```




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793076686



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +

Review comment:
       None of the other Redis data types use that, unfortunately. I'm reluctant to use a new scheme here, but perhaps I can open up a ticket to change our implementation to JVMSizeUtils across the board.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {
+      DataSerializer.writeByteArray((byte[]) element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      elementPush(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  public synchronized byte[] elementRemove(int index) {
+    return elementList.remove(index);
+  }
+
+  public synchronized boolean elementRemove(byte[] element) {
+    return elementList.remove(element);
+  }
+
+  public synchronized void elementPush(byte[] element) {
+    elementList.addFirst(element);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elementList.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elementList.size() != elementList.size()) {
+      return false;
+    }
+    for (Object element : elementList) {

Review comment:
       Tweaked.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793055969



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.data.delta;
+
+import static org.apache.geode.DataSerializer.readInteger;
+import static org.apache.geode.internal.InternalDataSerializer.readArrayLength;
+import static org.apache.geode.redis.internal.data.delta.DeltaType.REMOVE_ELEMENTS_BY_INDEX;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.internal.InternalDataSerializer;
+import org.apache.geode.redis.internal.data.AbstractRedisData;
+
+public class RemoveElementsByIndex implements DeltaInfo {
+  private final List<Integer> indexes;
+
+  public void add(int index) {
+    indexes.add(index);
+  }
+
+  public RemoveElementsByIndex() {
+    this.indexes = new ArrayList<>();
+  }
+
+  public void serializeTo(DataOutput out) throws IOException {
+    DataSerializer.writeEnum(REMOVE_ELEMENTS_BY_INDEX, out);
+    InternalDataSerializer.writeArrayLength(indexes.size(), out);
+    for (int index : indexes) {
+      DataSerializer.writeInteger(index, out);
+    }
+  }
+
+  public static void deserializeFrom(DataInput in, AbstractRedisData redisData) throws IOException {
+    int size = readArrayLength(in);
+    List<Integer> indexes = new ArrayList<>();
+    while (size > 0) {
+      indexes.add(readInteger(in));

Review comment:
       Done. Avoiding object allocation is probably better than avoiding multiple function calls.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.data.delta;
+
+import static org.apache.geode.DataSerializer.readInteger;
+import static org.apache.geode.internal.InternalDataSerializer.readArrayLength;
+import static org.apache.geode.redis.internal.data.delta.DeltaType.REMOVE_ELEMENTS_BY_INDEX;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.internal.InternalDataSerializer;
+import org.apache.geode.redis.internal.data.AbstractRedisData;
+
+public class RemoveElementsByIndex implements DeltaInfo {
+  private final List<Integer> indexes;
+
+  public void add(int index) {
+    indexes.add(index);
+  }
+
+  public RemoveElementsByIndex() {
+    this.indexes = new ArrayList<>();
+  }
+
+  public void serializeTo(DataOutput out) throws IOException {
+    DataSerializer.writeEnum(REMOVE_ELEMENTS_BY_INDEX, out);
+    InternalDataSerializer.writeArrayLength(indexes.size(), out);
+    for (int index : indexes) {
+      DataSerializer.writeInteger(index, out);

Review comment:
       Done.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788177387



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopArgumentCountIntegrationTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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 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 org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+// We only need this test because LPop has a different argument count between Redis 5.x
+// and Redis 6.2+. But the pipeline is testing against Redis 6.2.x. Once the "COUNT"
+// subcommand is implemented for LPOP, this test (and LPopArgumentCountIntegrationTest)
+// should be terminated with prejudice

Review comment:
       Package-private should be sufficient rather than public, but it doesn't really matter that much for test code.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] dschneider-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
dschneider-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r785181426



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/GeodeRedisService.java
##########
@@ -60,6 +60,7 @@ public void register(DataSerializableFixedIdRegistrar registrar) {
     registrar.register(DataSerializableFixedID.REDIS_KEY, RedisKey.class);
     registrar.register(DataSerializableFixedID.PUBLISH_REQUEST,
         Publisher.PublishRequest.class);
+    registrar.register(DataSerializableFixedID.REDIS_LIST_ID, RedisSet.class);

Review comment:
       This should be RedisList.class instead of RedisSet.class

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableList.java
##########
@@ -0,0 +1,78 @@
+/*
+ * 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.data.collections;
+
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableList<K> extends LinkedList<K> implements Sizeable {
+  private static final long serialVersionUID = -8255284217236712060L;
+  private static final int SIZEABLE_LIST_OVERHEAD =
+      memoryOverhead(SizeableList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+
+  private int memberOverhead;
+
+  public SizeableList(Collection<K> collection) {

Review comment:
       It seems wrong that given a collection we will add that collection in reverse order. I know that is what LPUSH needs but what happens when we implement RPUSH? It seems like it would be cleaner to get rid of these constructors that take a Collection and instead just have a zero-arg constructor. Given that this is a LinkedList we don't lose anything by not telling the constructor an initial size. Once everyone uses a zero-arg constructor then the caller who constructs it will take care of adding its elements in the order it desires. You should probably just have something like addHead and addTail for LPUSH and RPUSH.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();

Review comment:
       Also we don't want to ship the entire element removed by pop to the secondary. An index sounds good

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);

Review comment:
       this is okay for now but will have a problem in the future when RPUSH is implemented. Given a bunch of byte arrays to add you will need some extra info about the order to add them and/or the index to add them

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!
+    for (int i = 0; i < size; ++i) {
+      elements.add(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  @VisibleForTesting
+  synchronized boolean appendElement(byte[] elementToAdd) {
+    return elements.add(elementToAdd);
+  }
+
+  @VisibleForTesting
+  synchronized boolean prependElement(byte[] elementToAdd) {
+    elements.add(0, elementToAdd);
+    return true;
+  }
+
+  @VisibleForTesting
+  synchronized boolean elementRemove(byte[] memberToRemove) {
+    return elements.remove(memberToRemove);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elements.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elements.size() != elements.size()) {

Review comment:
       consider adding equals and hashCode on ElementList. The size and element comparison would be the job of ElementList.equals

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);

Review comment:
       consider having private synchronized methods (like you did with prependElement and appendElement) for all places that will modify "elements". Or maybe these should be methods on ElementList. This class actually exposes a lot of stuff it inherits from LinkedList. It might be better for it to own a LinkedList instead of subclassing. Also consider making ElementList and SizableList a single class.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles merged pull request #7261: GEODE-9892: create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles merged pull request #7261:
URL: https://github.com/apache/geode/pull/7261


   


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794096487



##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
##########
@@ -0,0 +1,72 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+
+public class SizeableByteArrayListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  @Before
+  public void setup() {}

Review comment:
       This method can be removed.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int PRIME_NUMBER = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex() + 1;
+      hashCode = hashCode * (PRIME_NUMBER % index) + Arrays.hashCode(iterator.next());

Review comment:
       The `(PRIME_NUMBER % index)` part of this hashCode calculation will be the same for all lists, so it seems unnecessary. The hashCode method should then become:
   ```
     public int hashCode() {
       final int primeNumber = 31;
       int hashCode = 1;
       for (byte[] bytes : this) {
         hashCode = primeNumber * hashCode + Arrays.hashCode(bytes);
       }
       return hashCode;
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    assertThat(jedis.keys(KEY_WITH_TAG)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    jedis.lpop(KEY_WITH_TAG);
+    jedis.lpop(KEY_WITH_TAG);
+    assertThat(jedis.keys(KEY_WITH_TAG)).isEmpty();
+  }
+
+  @Test
+  public void lpop_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong lpopReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> lpopReference.set(jedis.llen(KEY)))
+            .runWithAction(() -> {
+              AssertionsForClassTypes.assertThat(lpopReference).satisfiesAnyOf(
+                  lpopResult -> AssertionsForClassTypes.assertThat(lpopResult.get())
+                      .isEqualTo(valuesInitial.length),
+                  lpopResult -> AssertionsForClassTypes.assertThat(lpopResult.get())
+                      .isEqualTo(valuesInitial.length + valuesToAdd.length));

Review comment:
       This test should be doing an LPOP, not LLEN, and the assertions to should be adjusted accordingly. Additionally, `AssertionsForClassTypes` should not be used, but rather just `Assertions`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/data/AbstractMemoryOverheadIntegrationTest.java
##########
@@ -239,7 +276,7 @@ private void measureAndCheckPerEntryOverhead(AddEntryFunction addEntry, Measurem
     // Put some entries to make sure we initialize any constant size data structures. We are
     // just trying to measure the cost of each add entry operation.
     for (int i = 0; i < WARM_UP_ENTRY_COUNT; i++) {
-      String uniqueString = String.format("warmup-%10d", i);
+      String uniqueString = String.format("warmup-%010d", i);

Review comment:
       Was this change intentional? What's the purpose of it?

##########
File path: geode-for-redis/src/main/resources/org/apache/geode/redis/internal/services/sanctioned-geode-for-redis-serializables.txt
##########
@@ -3,6 +3,7 @@ org/apache/geode/redis/internal/commands/executor/BaseSetOptions$Exists,false
 org/apache/geode/redis/internal/commands/parameters/RedisParametersMismatchException,true,-643700717871858072
 org/apache/geode/redis/internal/data/RedisDataType,false,nullType:org/apache/geode/redis/internal/data/RedisData,toStringValue:java/lang/String
 org/apache/geode/redis/internal/data/RedisSortedSet$MemberAddResult,false
+org/apache/geode/redis/internal/data/collections/SizeableByteArrayList,false,memberOverhead:int

Review comment:
       This should be in `excluded-classes` instead of this file, as we never expect to need to serialize or deserialize `SizeableByteArrayList`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;

Review comment:
       This can be package private instead of public.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLLenIntegrationTest.java
##########
@@ -0,0 +1,124 @@
+/*
+ * 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_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLLenIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 llen_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.llen("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void llen_givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LLEN, 1);
+  }
+
+  @Test
+  public void llen_givenNonexistentList_returnsZero() {
+    assertThat(jedis.llen("nonexistent")).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_returnsListLength() {
+    jedis.lpush(KEY, "e1", "e2", "e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(3L);
+
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e3");

Review comment:
       Not a major issue, but with these asserts after each LPOP, this test could fail due to a problem with LPOP, despite LLEN having correct behaviour. To keep tests more focused, it's better to have them only assert on things that they're explicitly trying to test the behaviour of.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLLenIntegrationTest.java
##########
@@ -0,0 +1,124 @@
+/*
+ * 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_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLLenIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 llen_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.llen("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void llen_givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LLEN, 1);
+  }
+
+  @Test
+  public void llen_givenNonexistentList_returnsZero() {
+    assertThat(jedis.llen("nonexistent")).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_returnsListLength() {
+    jedis.lpush(KEY, "e1", "e2", "e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(3L);
+
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(2L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+    assertThat(jedis.llen(KEY)).isEqualTo(1L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e1");
+    assertThat(jedis.llen(KEY)).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong llenReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> llenReference.set(jedis.llen(KEY)))
+            .runWithAction(() -> {
+              AssertionsForClassTypes.assertThat(llenReference).satisfiesAnyOf(
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())
+                      .isEqualTo(valuesInitial.length),
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())

Review comment:
       These assertions should not be using `AssertionsForClassTypes` but rather the vanilla `Assertions` method.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,184 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementPush(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementRemove(0);
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (byte[] element : elementList) {
+      DataSerializer.writeByteArray(element, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    SizeableByteArrayList tempElementList = new SizeableByteArrayList();
+    for (int i = 0; i < size; ++i) {
+      byte[] element = DataSerializer.readByteArray(in);
+      tempElementList.addLast(element);
+    }
+    replaceElementList(tempElementList);
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  private synchronized void replaceElementList(SizeableByteArrayList newList) {
+    elementList = newList;
+  }

Review comment:
       I think this method might be overkill, since we're not modifying the contents of `elementList` here, just replacing it entirely, so there's no danger of `toData` being affected.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int PRIME_NUMBER = 31;

Review comment:
       This variable should not be in all-caps, as that is reserved for static final constants. The [Geode Code Style Guide page](https://cwiki.apache.org/confluence/display/GEODE/Code+Style+Guide) on the wiki contains a link to the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) which details the conventions we (try to) follow when writing code for Geode.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
##########
@@ -0,0 +1,43 @@
+/*
+ * 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 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.commands.parameters.RedisParametersMismatchException;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LPopExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    // TODO: Size check needed until we implement 'count' arg (Redis 6.2+)
+    if (command.getProcessedCommand().size() > 2) {
+      throw new RedisParametersMismatchException(command.wrongNumberOfArgumentsErrorMessage());
+    }

Review comment:
       See my comments in `RedisCommandType` regarding this. We should do our parameter count checking using the `Parameter()` class in `RedisCommandType`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLLenIntegrationTest.java
##########
@@ -0,0 +1,124 @@
+/*
+ * 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_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLLenIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 llen_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.llen("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void llen_givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LLEN, 1);
+  }
+
+  @Test
+  public void llen_givenNonexistentList_returnsZero() {
+    assertThat(jedis.llen("nonexistent")).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_returnsListLength() {
+    jedis.lpush(KEY, "e1", "e2", "e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(3L);
+
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(2L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+    assertThat(jedis.llen(KEY)).isEqualTo(1L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e1");
+    assertThat(jedis.llen(KEY)).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong llenReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> llenReference.set(jedis.llen(KEY)))
+            .runWithAction(() -> {
+              AssertionsForClassTypes.assertThat(llenReference).satisfiesAnyOf(
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())
+                      .isEqualTo(valuesInitial.length),
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())
+                      .isEqualTo(valuesInitial.length + valuesToAdd.length));
+              for (int i = 0; i < valuesToAdd.length; i++) {
+                jedis.lpop(KEY);
+              }
+            });
+  }
+
+  @Test
+  public void llen_withConcurrentLPop_returnsCorrectValue() {

Review comment:
       It's not possible to test concurrent behaviour with LPOP (at least, the version that only pops one element at a time) because there are only two possible states for the list when doing a single LPOP; unmodified, or with one element missing. Since there's no "in-between" state that might be encountered due to improper locking, it's not possible to test that code using LPOP, we just end up testing that LPOP removes an element. As such, this test should be removed.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;

Review comment:
       This would be more efficient as:
   ```
       ListIterator<byte[]> iterator = this.listIterator();
       while (iterator.hasNext()) {
         byte[] element = iterator.next();
         if (Arrays.equals(element, (byte[]) o)) {
           iterator.remove();
           memberOverhead -= calculateByteArrayOverhead((byte[]) o);
           return true;
         }
       }
       return false;
   ```
   as then we only have to iterate the list once.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_LIST;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+
+import org.junit.Test;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.serialization.ByteArrayDataInput;
+import org.apache.geode.internal.serialization.SerializationContext;
+
+public class RedisListTest {
+
+  @Test
+  public void confirmSerializationIsStable() throws IOException, ClassNotFoundException {
+    RedisList list1 = createRedisList(1, 2);
+    int expirationTimestamp = 1000;
+    list1.setExpirationTimestampNoDelta(expirationTimestamp);
+    HeapDataOutputStream out = new HeapDataOutputStream(100);
+    DataSerializer.writeObject(list1, out);
+    ByteArrayDataInput in = new ByteArrayDataInput(out.toByteArray());
+    RedisList list2 = DataSerializer.readObject(in);
+    assertThat(list2.getExpirationTimestamp())
+        .isEqualTo(list1.getExpirationTimestamp())
+        .isEqualTo(expirationTimestamp);
+    assertThat(list2).isEqualTo(list1);
+  }
+
+  @Test
+  public void confirmToDataIsSynchronized() throws NoSuchMethodException {
+    assertThat(Modifier
+        .isSynchronized(RedisList.class
+            .getMethod("toData", DataOutput.class, SerializationContext.class).getModifiers()))
+                .isTrue();
+  }
+
+  @Test
+  public void hashcode_returnsSameValue_forEqualLists() {
+    RedisList list1 = createRedisList(1, 2);
+    RedisList list2 = createRedisList(1, 2);
+    assertThat(list1).isEqualTo(list2);
+    assertThat(list1.hashCode()).isEqualTo(list2.hashCode());
+  }
+
+  @Test
+  public void hashcode_returnsDifferentValue_forDifferentLists() {
+    RedisList list1 = createRedisList(1, 2);
+    RedisList list2 = createRedisList(2, 1);
+    assertThat(list1).isNotEqualTo(list2);
+    assertThat(list1.hashCode()).isEqualTo(list2.hashCode());

Review comment:
       This should be using `isNotEqualTo()`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest

Review comment:
       We can have a test for argument count here:
   ```
     @Test
     public void lpop_givenWrongNumOfArgs_returnsError() {
       assertExactNumberOfArgs(jedis, Protocol.Command.LPOP, 2);
     }
   ```
   and then `@Override` it in `LPopIntegrationTest` so that we test against native Redis and against geode-for-redis, just with different implementations:
   ```
     @Test
     @Override
     public void lpop_givenWrongNumOfArgs_returnsError() {
       assertExactNumberOfArgs(jedis, Protocol.Command.LPOP, 1);
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {

Review comment:
       To avoid confusion about RedisString vs Java String here, could this test be renamed "lpop_withNonListKey_returnsWrongTypeError"?

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    assertThat(jedis.keys(KEY_WITH_TAG)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    jedis.lpop(KEY_WITH_TAG);
+    jedis.lpop(KEY_WITH_TAG);

Review comment:
       Could we have assertions on what's returned from each LPOP here, just to make sure that we're behaving as expected?

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {

Review comment:
       Could this test instead be "lpop_returnsLeftmostMember" since we care *which* member is returned, not just that one member is returned.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;
+
+    jedis.lpush(KEY_WITH_TAG, "e1");
+    jedis.lpop(KEY_WITH_TAG);
+    assertThat(jedis.keys(KEY_WITH_TAG)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;

Review comment:
       This variable is unnecessary.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {

Review comment:
       Having both of these test cases is redundant, as the second one entirely contains the first one. It should be okay to just have the second test, and update the name to "lpush_withExistingKey_ofWrongType_returnsWrongTypeError_shouldNotOverWriteExistingKey"

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,139 @@
+/*
+ * 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.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 java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {
+  private static final String KEY_BASE = "key";
+  private static final int NUM_LISTS = 100;
+  private static final String ELEMENT_BASE = "element-";
+  public static final int NUM_VMS = 4;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final int LIST_SIZE = 100;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash()
+      throws Exception {
+    List<String> elementList1 = makeElementList(0, LIST_SIZE / 2, ELEMENT_BASE);
+    List<String> elementList2 = makeElementList(LIST_SIZE / 2, LIST_SIZE, ELEMENT_BASE);
+    List<String> allElements = makeElementList(0, LIST_SIZE, ELEMENT_BASE);
+
+    new ConcurrentLoopingThreads(NUM_LISTS / 2,
+        (i) -> jedis.lpush(KEY_BASE + i, elementList1.toArray(new String[] {})),
+        (i) -> jedis.lpush(KEY_BASE + i, elementList2.toArray(new String[] {})),
+        (i) -> jedis.lpush(KEY_BASE + (i + NUM_LISTS / 2), elementList1.toArray(new String[] {})),
+        (i) -> jedis.lpush(KEY_BASE + (i + NUM_LISTS / 2), elementList2.toArray(new String[] {})))
+            .runInLockstep();
+
+    clusterStartUp.crashVM(NUM_VMS - 1);
+
+    confirmAllDataIsPresent(allElements);
+  }

Review comment:
       This test is not testing the distributed behaviour of LPOP, but rather testing that secondary copies of buckets contain the same data as primary copies after concurrent LPUSH calls, so that when we failover due to the server crash, data is consistent. As such, this test should be reworked to test distributed LPOP behaviour. Examples of what we could test:
   - With two servers, loading a list on one server, calling LPOP repeatedly to empty half the list, then crashing the primary server results in the list containing what we expect it to after the secondary is promoted to primary
   - With at least three servers, loading a list, then concurrently calling LPOP while forcing the primary to move to a member that doesn't host the bucket (similar to what's done in `StringsDUnitTest.givenBucketsMoveDuringAppend_thenDataIsNotLost`) and asserting that we never get the same element returned from LPOP twice (which would indicate that we did an LPOP without actually removing the element).
   
   Given that LPOP can suffer from the same problems as APPEND, where a retried command results in the operation being performed twice, care needs to be taken with what we assert in the second test case so as not to produce a flaky test.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;

Review comment:
       This variable is unnecessary, since commands using only one key in integration tests do not need to use tags.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
##########
@@ -286,6 +289,12 @@
   ZUNIONSTORE(new ZUnionStoreExecutor(), SUPPORTED,
       new Parameter().min(4).flags(WRITE, DENYOOM, MOVABLEKEYS)),
 
+  /************** Lists *****************/
+
+  LLEN(new LLenExecutor(), SUPPORTED, new Parameter().exact(2).flags(READONLY, FAST)),
+  LPOP(new LPopExecutor(), SUPPORTED, new Parameter().min(2).flags(WRITE, FAST)),

Review comment:
       I still feel that this should be changed to `exact(2)` as that's the behaviour we're requiring from the user, and the output of the COMMAND command will reflect what's encoded here. If we have the COMMAND command say that LPOP takes 2 or more arguments, but do not accept more than 2 arguments, that's a bug.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {
+    byte[] blob = new byte[256];
+    for (int i = 0; i < 256; i++) {
+      blob[i] = (byte) i;
+    }
+
+    jedis.lpush(KEY.getBytes(), blob, blob);
+
+    byte[] result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+    result = jedis.lpop(KEY.getBytes());
+    assertThat(result).containsExactly(blob);
+  }
+
+  @Test
+  public void lpush_returnsUpdatedListLength() {
+    Long result = jedis.lpush(KEY, "e1");
+    assertThat(result).isEqualTo(jedis.llen(KEY));
+
+    result = jedis.lpush(KEY, "e2");
+    assertThat(result).isEqualTo(jedis.llen(KEY));
+
+    result = jedis.lpush(KEY, "e3");
+    assertThat(result).isEqualTo(jedis.llen(KEY));

Review comment:
       These assertions should be using hardcoded integer values rather than the returned value of LLEN, as it's possible that LLEN and LPUSH could both be broken in such a way that this test passes, but that the behaviour isn't correct. It would also be good to have one of the LPUSH calls use multiple elements to make sure that the returned value is as expected then too. The test would then become:
   ```
     @Test
     public void lpush_returnsUpdatedListLength() {
       assertThat(jedis.lpush(KEY, "e1")).isEqualTo(1);
   
       assertThat(jedis.lpush(KEY, "e2")).isEqualTo(2);
   
       assertThat(jedis.lpush(KEY, "e3", "e4")).isEqualTo(4);
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpushErrors_withExistingKey_ofWrongType() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue))
+        .hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);
+
+    String result = jedis.get(KEY);
+
+    assertThat(result).isEqualTo(PREEXISTING_VALUE);
+  }
+
+  @Test
+  public void lpush_canStoreBinaryData() {

Review comment:
       This test is unnecessary, as the format of the data passed to the geode-for-redis server is determined by the client sending the command. If there was a difference in behaviour between LPUSH with a String and LPUSH with a byte[], it would be due to a bug with Jedis, not with geode-for-redis.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.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 java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPushDUnitTest {
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+  private static final int LIST_SIZE = 1000;
+  private static JedisCluster jedis;
+
+  private static MemberVM server1;
+  private static MemberVM server2;
+  private static MemberVM server3;
+
+  @BeforeClass
+  public static void classSetup() {
+    MemberVM locator = clusterStartUp.startLocatorVM(0);
+    server1 = clusterStartUp.startRedisVM(1, locator.getPort());
+    server2 = clusterStartUp.startRedisVM(2, locator.getPort());
+    server3 = clusterStartUp.startRedisVM(3, locator.getPort());
+
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+
+    server1.stop();
+    server2.stop();
+    server3.stop();

Review comment:
       This is not necessary, as shutdown is handled by the `RedisClusterStartupRule`.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r799670343



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();

Review comment:
       Simplified.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isEqualTo("e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();

Review comment:
       Done.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: GEODE-9892: create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1033985614


   This pull request **introduces 2 alerts** when merging b15187b3ee04e57aad77b6e12e8d42d2ed6f4cb7 into ce57e9fd2b8b644cadc469209e12e4fbd281e0d9 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-b9275cb6c7ca99673e00b18bfaceb59748518468)
   
   **new alerts:**
   
   * 2 for Implicit conversion from array to string


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r797930936



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,90 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +

Review comment:
       Yes, I've pushed up an update already. :)




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1012446785


   This pull request **introduces 1 alert** when merging 2160141961cbfc96a00429194afb5b8fb9a88ab5 into 2b032440eb9d0f3c68a94602289cc41435c68fad - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-4a8c71913c0e551160dbe84c581ce0eff845c27f)
   
   **new alerts:**
   
   * 1 for Spurious Javadoc @param tags


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788176020



##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }
+
+  private RedisList.ElementList createTestElementList(int size) {
+    List<byte[]> initialElements = new ArrayList<>(size);
+    for (int i = 0; i < size; ++i) {
+      initialElements.add(new byte[] {(byte) i});
+    }
+    RedisList.ElementList list = new RedisList.ElementList(initialElements);
+    return list;
+  }
+
+  @Test
+  public void getSizeInBytesForEmptyElementList() {
+    RedisList.ElementList list = createTestElementList(0);

Review comment:
       This class is pining for the fjords.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableListTest.java
##########
@@ -0,0 +1,76 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.data.RedisList;
+
+public class SizeableListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  private int expectedSize(SizeableList list) {
+    return sizer.sizeof(list);
+  }
+
+  private RedisList.ElementList createTestElementList(int size) {
+    List<byte[]> initialElements = new ArrayList<>(size);
+    for (int i = 0; i < size; ++i) {
+      initialElements.add(new byte[] {(byte) i});
+    }
+    RedisList.ElementList list = new RedisList.ElementList(initialElements);
+    return list;
+  }
+
+  @Test
+  public void getSizeInBytesForEmptyElementList() {
+    RedisList.ElementList list = createTestElementList(0);
+    assertThat(list.getSizeInBytes()).isEqualTo(expectedSize(list));
+  }
+
+  @Test
+  public void getSizeInBytesIsAccurateForByteArrays() {
+    List<byte[]> initialElements = new ArrayList<>();
+    int initialNumberOfElements = 20;
+    int elementsToAdd = 100;
+    for (int i = 0; i < initialNumberOfElements; ++i) {
+      initialElements.add(new byte[] {(byte) i});
+    }
+    // Create a set with an initial size and confirm that it correctly reports its size
+    RedisList.ElementList list = new RedisList.ElementList(initialElements);
+    assertThat(list.getSizeInBytes()).isEqualTo(expectedSize(list));
+
+    // Add enough members to force a resize and assert that the size is correct after each add

Review comment:
       Excised

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/netty/ExecutionHandlerContext.java
##########
@@ -483,17 +484,25 @@ private RedisSortedSet getRedisSortedSet(RedisKey key, boolean updateStats) {
     return getRegionProvider().getTypedRedisData(RedisDataType.REDIS_SORTED_SET, key, updateStats);
   }
 
-
   public <R> R setLockedExecute(RedisKey key, boolean updateStats,
       FailableFunction<RedisSet, R> function) {
     return getRegionProvider().lockedExecute(key,
         () -> function.apply(getRedisSet(key, updateStats)));
   }
 
+  public <R> R listLockedExecute(RedisKey key, boolean updateStats,
+      FailableFunction<RedisList, R> function) {
+    return getRegionProvider().lockedExecute(key,
+        () -> function.apply(getRedisList(key, updateStats)));
+  }
+
   public RedisSet getRedisSet(RedisKey key, boolean updateStats) {
     return getRegionProvider().getTypedRedisData(RedisDataType.REDIS_SET, key, updateStats);
   }
 
+  public RedisList getRedisList(RedisKey key, boolean updateStats) {

Review comment:
       Privatized.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1017755558


   This pull request **introduces 1 alert** when merging 27d6f6c6d90754cf17a5a225049da1936d00637d into b349cb8b33fb0c93c0a3ea6a2f6c32bd33b51319 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-8a48db218aecae892f73d02e3d2f582ed0419d7f)
   
   **new alerts:**
   
   * 1 for Equals or hashCode on arrays


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r793078608



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,187 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableByteArrayList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private SizeableByteArrayList elementList;
+
+  public RedisList() {
+    this.elementList = new SizeableByteArrayList();
+  }
+
+  /**
+   * @param elementsToAdd elements to add to this set; NOTE this list may by modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      elementList.addFirst(element);
+    }
+    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    return elementList.size();
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    byte[] popped = elementList.removeFirst();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    removed.add(0);
+    storeChanges(region, key, removed);
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen() {
+    return elementList.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    elementPush(bytes);
+  }
+
+  @Override
+  public void applyRemoveElementsByIndex(List<Integer> indexes) {
+    for (int index : indexes) {
+      elementRemove(index);
+    }
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
+   * object, the striped executor will not protect toData. So any methods that modify "elements"
+   * needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elementList.size(), out);
+    for (Object element : elementList) {

Review comment:
       Yup, fixed.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794943571



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLLenIntegrationTest.java
##########
@@ -0,0 +1,124 @@
+/*
+ * 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_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLLenIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 llen_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.llen("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void llen_givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LLEN, 1);
+  }
+
+  @Test
+  public void llen_givenNonexistentList_returnsZero() {
+    assertThat(jedis.llen("nonexistent")).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_returnsListLength() {
+    jedis.lpush(KEY, "e1", "e2", "e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(3L);
+
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(2L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+    assertThat(jedis.llen(KEY)).isEqualTo(1L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e1");
+    assertThat(jedis.llen(KEY)).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong llenReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> llenReference.set(jedis.llen(KEY)))
+            .runWithAction(() -> {
+              AssertionsForClassTypes.assertThat(llenReference).satisfiesAnyOf(
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())
+                      .isEqualTo(valuesInitial.length),
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())
+                      .isEqualTo(valuesInitial.length + valuesToAdd.length));
+              for (int i = 0; i < valuesToAdd.length; i++) {
+                jedis.lpop(KEY);
+              }
+            });
+  }
+
+  @Test
+  public void llen_withConcurrentLPop_returnsCorrectValue() {

Review comment:
       Yanked.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;

Review comment:
       Protected, now. Needs to be visible in LPopIntegrationTest.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest

Review comment:
       It would be assertAtMostNArgs() for native Redis, but this works pretty well. Left in comments just to make clear what's up. I know I won't remember this in six months.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {

Review comment:
       Elucidated.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r799670725



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopAndCrashesDUnitTest.java
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.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 org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopAndCrashesDUnitTest {

Review comment:
       Was able to combine things with a little tweaking.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: GEODE-9892: create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r807460668



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,116 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicReference;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);

Review comment:
       Could this assertion be changed to be `hasMessage()` so as to be more explicit please?

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));

Review comment:
       Could this be changed to 
   ```
   assertThat(jedis.lpop(key)).isEqualTo(elementList.get(0));
   ```
   since we already have the list of elements we added, so we don't need to create a new element to check here.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,116 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicReference;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String key = KEY;
+
+    jedis.lpush(key, "e1");
+    assertThat(jedis.lpop(key)).isEqualTo("e1");
+    assertThat(jedis.lpop(key)).isNull();
+    assertThat(jedis.lpop(key)).isNull();
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void lpop_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "troix"};
+    String[] valuesToAdd = new String[] {"plum", "peach", "orange"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicReference<String> lpopReference = new AtomicReference<>();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> lpopReference.set(jedis.lpop(KEY)))
+            .runWithAction(() -> {
+              assertThat(lpopReference).satisfiesAnyOf(
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("orange"),
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("troix"),
+                  lpopResult -> assertThat(lpopReference.get()).isNull());

Review comment:
       I'm not sure under what circumstances we would ever expect LPOP to return null in this test, since the list always contains something. If we ever do return null, then something is wrong.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();

Review comment:
       Compiler warnings here and elsewhere in the file can be fixed by using `Future<Void>`

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }

Review comment:
       Since we create the JedisCluster in the `@Before` method, we should probably call `close()` on it in an `@After` rather than an `@AfterClass` method.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPushIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtLeastNArgs;
+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 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.RedisIntegrationTest;
+
+public abstract class AbstractLPushIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 lpushErrors_givenTooFewArguments() {
+    assertAtLeastNArgs(jedis, Protocol.Command.LPUSH, 2);
+  }
+
+  @Test
+  public void lpush_withExistingKey_ofWrongType_returnsWrongTypeError_shouldNotOverWriteExistingKey() {
+    String elementValue = "list element value that should never get added";
+
+    jedis.set(KEY, PREEXISTING_VALUE);
+
+    assertThatThrownBy(() -> jedis.lpush(KEY, elementValue)).isInstanceOf(JedisDataException.class);

Review comment:
       Could we also assert on the message returned with the error here, using `hasMessage()`?

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed

Review comment:
       Rather than adding this line here, it would be better to move all cluster set-up to the `@Before` method and make the `RedisClusterStartupRule` a `@Rule` rather than a `@ClassRule`. This will make the tests take a little longer, since we have to recreate the cluster every time rather than just server1, but it means that the `RedisClusterStartupRule` will handle shutting down all the members at the end of each test, so there's no chance of test order mucking things up. This also applies to LPopDUnitTest, which has a similar potential issue around test ordering.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), MINIMUM_ITERATIONS);
+    List<String> elementList2 = makeElementList(keys.get(1), MINIMUM_ITERATIONS);
+    List<String> elementList3 = makeElementList(keys.get(2), MINIMUM_ITERATIONS);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(keys.get(2), runningCount);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);

Review comment:
       This is kind of a misuse of `await()` since nothing's actually being awaited on. It would be better to just explicitly use a `Thread.sleep()` since that's what ends up getting called under the covers anyway, but makes it more obvious what the code is trying to achieve. Either that, or find something specific to assert on that ensures we're not moving buckets too quickly.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()

Review comment:
       This test might be better named something like "givenBucketsMovedDuringLPush_elementsAreAddedAtomically" since the buckets moving part is integral to triggering the potential issues.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);

Review comment:
       This assertion is redundant, since it's not possible for all three keys to have lengths greater than or equal to `MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE` but the sum of those three values to be less than `MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE`.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), MINIMUM_ITERATIONS);
+    List<String> elementList2 = makeElementList(keys.get(1), MINIMUM_ITERATIONS);
+    List<String> elementList3 = makeElementList(keys.get(2), MINIMUM_ITERATIONS);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> lpopPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> lpopPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> lpopPerformAndVerify(keys.get(2), runningCount);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    runningCount.set(0);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("lpop", 3));
+    return listHashtags;
+  }
+
+  private List<String> makeListKeys(List<String> listHashtags) {
+    List<String> keys = new ArrayList<>();
+    keys.add(makeListKeyWithHashtag(1, listHashtags.get(0)));
+    keys.add(makeListKeyWithHashtag(2, listHashtags.get(1)));
+    keys.add(makeListKeyWithHashtag(3, listHashtags.get(2)));
+    return keys;
+  }
+
+
+  private void lpushPerformAndVerify(String key, List<String> elementList) {
+    jedis.lpush(key, elementList.toArray(new String[] {}));
+
+    Long listLength = jedis.llen(key);
+    assertThat(listLength).isEqualTo(elementList.size())
+        .as("Initial list lengths not equal for key %s'", key);

Review comment:
       The `as()` needs to come after the `assertThat()` rather than the `isEqualTo()`. From the JavaDoc for `as()`:
   ```
      * Sets the description of the assertion that is going to be called after.
      * <p>
      * You must set it <b>before</b> calling the assertion otherwise it is ignored as the failing assertion breaks
      * the chained call by throwing an AssertionError.
   ```

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }

Review comment:
       It's not clear to me how this test is verifying that list elements are added atomically, since it doesn't verify the contents of the lists, just their lengths. We could have a situation where we do two LPUSH operations with elements `1, 1, 1` and `a, a, a`, and end up with a list `1, 1, a, a, a, 1` and the test wouldn't mind. Would it be possible to add validation that every three consecutive elements of the list correspond to one single LPUSH operation? To make it easier, you could push three identical but distinct elements each time, like the example I gave, then just check that every three list elements are equal to each other to make sure nothing got applied out of order.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed
+  }
+
+  private void lpushPerformAndVerify(String key, List<String> elementList,
+      AtomicLong runningCount) {
+    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+      long listLength = jedis.llen(key);
+      long newLength = jedis.lpush(key, elementList.toArray(new String[] {}));
+      assertThat((newLength - listLength) % 3).as("LPUSH, list length %s not multiple of 3",
+          newLength).isEqualTo(0);
+    }
+    runningCount.decrementAndGet();
+  }
+
+  @Test
+  public void shouldDistributeElementsAcrossCluster()

Review comment:
       This test would be better named something like "shouldNotLoseData_givenPrimaryServerCrashesDuringOperations" since that's what we're really trying to test here.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);

Review comment:
       See comment in `LPopDUnitTest` about using `Thread.sleep()` or asserting on something meaningful here.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPopDUnitTest {
+  public static final int MINIMUM_ITERATIONS = 10000;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("lpop", 1));
+    List<String> elementList = makeElementList(key, MINIMUM_ITERATIONS);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    for (int i = MINIMUM_ITERATIONS - 1; i > 0; i--) {
+      assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, i));
+    }
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lpop(key)).isEqualTo(makeElementString(key, 0));
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLpop_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), MINIMUM_ITERATIONS);
+    List<String> elementList2 = makeElementList(keys.get(1), MINIMUM_ITERATIONS);
+    List<String> elementList3 = makeElementList(keys.get(2), MINIMUM_ITERATIONS);

Review comment:
       The name `MINIMUM_ITERATIONS` is a bit misleading, since we actually add exactly that many elements to the list every time. It should probably be renamed to something like "INITIAL_LIST_SIZE" and any assertions on it using `.isGreaterThanOrEqualTo()` should be changed to `.isEqualTo()`

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed
+  }
+
+  private void lpushPerformAndVerify(String key, List<String> elementList,
+      AtomicLong runningCount) {
+    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+      long listLength = jedis.llen(key);
+      long newLength = jedis.lpush(key, elementList.toArray(new String[] {}));
+      assertThat((newLength - listLength) % 3).as("LPUSH, list length %s not multiple of 3",
+          newLength).isEqualTo(0);
+    }
+    runningCount.decrementAndGet();
+  }
+
+  @Test
+  public void shouldDistributeElementsAcrossCluster()
+      throws ExecutionException, InterruptedException {
+    final int pusherCount = 6;
+    final int pushListSize = 3;
+    AtomicLong runningCount = new AtomicLong(pusherCount);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(pushListSize, "element1-");
+    List<String> elements2 = makeElementList(pushListSize, "element2-");
+    List<String> elements3 = makeElementList(pushListSize, "element2-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);

Review comment:
       This await should be on something meaningful, such as the length of the lists in the test (making sure we've added some minimum number of elements, for example) as otherwise the test could become flaky.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -0,0 +1,216 @@
+/*
+ * 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.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 java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LPushDUnitTest {
+  public static final int PUSHER_COUNT = 6;
+  public static final int PUSH_LIST_SIZE = 3;
+  private static MemberVM locator;
+
+  @ClassRule
+  public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static final int MINIMUM_ITERATIONS = 10000;
+  private static JedisCluster jedis;
+
+  @BeforeClass
+  public static void classSetup() {
+    locator = clusterStartUp.startLocatorVM(0);
+    clusterStartUp.startRedisVM(2, locator.getPort());
+    clusterStartUp.startRedisVM(3, locator.getPort());
+  }
+
+  @Before
+  public void testSetup() {
+    clusterStartUp.startRedisVM(1, locator.getPort());
+    clusterStartUp.rebalanceAllRegions();
+    int redisServerPort = clusterStartUp.getRedisPort(1);
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort), REDIS_CLIENT_TIMEOUT);
+    clusterStartUp.flushAll();
+  }
+
+  @AfterClass
+  public static void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldPushMultipleElementsAtomically()
+      throws ExecutionException, InterruptedException {
+    AtomicLong runningCount = new AtomicLong(PUSHER_COUNT);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(PUSH_LIST_SIZE, "element1-");
+    List<String> elements2 = makeElementList(PUSH_LIST_SIZE, "element2-");
+    List<String> elements3 = makeElementList(PUSH_LIST_SIZE, "element3-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    for (int i = 0; i < 50 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      GeodeAwaitility.await().during(Duration.ofMillis(500)).until(() -> true);
+    }
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    int totalLength = 0;
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      totalLength += length;
+    }
+    assertThat(totalLength)
+        .isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * PUSHER_COUNT * PUSH_LIST_SIZE);
+    clusterStartUp.crashVM(1); // kill primary server, just in case test order is reversed
+  }
+
+  private void lpushPerformAndVerify(String key, List<String> elementList,
+      AtomicLong runningCount) {
+    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+      long listLength = jedis.llen(key);
+      long newLength = jedis.lpush(key, elementList.toArray(new String[] {}));
+      assertThat((newLength - listLength) % 3).as("LPUSH, list length %s not multiple of 3",
+          newLength).isEqualTo(0);
+    }
+    runningCount.decrementAndGet();
+  }
+
+  @Test
+  public void shouldDistributeElementsAcrossCluster()
+      throws ExecutionException, InterruptedException {
+    final int pusherCount = 6;
+    final int pushListSize = 3;
+    AtomicLong runningCount = new AtomicLong(pusherCount);
+
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+    List<String> elements1 = makeElementList(pushListSize, "element1-");
+    List<String> elements2 = makeElementList(pushListSize, "element2-");
+    List<String> elements3 = makeElementList(pushListSize, "element2-");
+
+    List<Runnable> taskList = new ArrayList<>();
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(0), elements1, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(1), elements2, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> lpushPerformAndVerify(keys.get(2), elements3, runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(0), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(1), runningCount));
+    taskList.add(() -> verifyListLengthCondition(keys.get(2), runningCount));
+
+    List<Future> futureList = new ArrayList<>();
+    for (Runnable task : taskList) {
+      futureList.add(executor.runAsync(task));
+    }
+
+    GeodeAwaitility.await().during(Duration.ofMillis(200)).until(() -> true);
+    clusterStartUp.crashVM(1); // kill primary server
+
+    for (Future future : futureList) {
+      future.get();
+    }
+
+    Long length;
+    for (String key : keys) {
+      length = jedis.llen(key);
+      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * pushListSize);
+      assertThat(length % 3).isEqualTo(0);
+    }

Review comment:
       This test should also validate the contents of the lists in some way, to make sure we haven't added elements out of order or interleaved different LPUSH commands.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r799670516



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,118 @@
+/*
+ * 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.assertAtMostNArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  protected JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // Overridden in LPopIntegrationTest until we implement Redis 6.2+ semantics
+  @Test
+  public void lpop_givenWrongNumOfArgs_returnsError() {
+    assertAtMostNArgs(jedis, Protocol.Command.LPOP, 2);
+  }
+
+  @Test
+  public void lpop_withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsLeftmostMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    jedis.lpop(keyWithTagForKeysCommand);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+
+    jedis.lpush(keyWithTagForKeysCommand, "e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isEqualTo("e1");
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.lpop(keyWithTagForKeysCommand)).isNull();
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void lpop_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong lpopReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> lpopReference.set(jedis.llen(KEY)))

Review comment:
       Lost this among the other changes. Restored.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794943416



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;

Review comment:
       I think it's slightly clearer if we pass 'element' to 'calculateByteArrayOverhead()' but otherwise good catch!

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int PRIME_NUMBER = 31;

Review comment:
       camelCased.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.data.collections;
+
+import static org.apache.geode.internal.JvmSizeUtils.getObjectHeaderSize;
+import static org.apache.geode.internal.JvmSizeUtils.getReferenceSize;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.internal.JvmSizeUtils.roundUpSize;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+import org.apache.geode.internal.size.Sizeable;
+
+public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeable {
+  private static final int BYTE_ARRAY_LIST_OVERHEAD = memoryOverhead(SizeableByteArrayList.class);
+  private static final int NODE_OVERHEAD =
+      roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
+  private static final int BYTE_ARRAY_BASE_OVERHEAD = 16;
+  private int memberOverhead;
+
+  @Override
+  public int indexOf(Object o) {
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex();
+      byte[] element = iterator.next();
+      if (Arrays.equals(element, (byte[]) o)) {
+        return index;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public int lastIndexOf(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    int index = indexOf(o);
+    if (index == -1) {
+      return false;
+    }
+    memberOverhead -= calculateByteArrayOverhead((byte[]) o);
+    remove(index);
+    return true;
+  }
+
+  @Override
+  public byte[] remove(int index) {
+    byte[] element = super.remove(index);
+    memberOverhead -= calculateByteArrayOverhead(element);
+    return element;
+  }
+
+  @Override
+  public void addFirst(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addFirst(element);
+  }
+
+  @Override
+  public void addLast(byte[] element) {
+    memberOverhead += calculateByteArrayOverhead(element);
+    super.addLast(element);
+  }
+
+  public boolean removeLastOccurrence(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  private int calculateByteArrayOverhead(byte[] element) {
+    return BYTE_ARRAY_BASE_OVERHEAD + (element.length % 8 == 0 ? 0 : 8) +
+        NODE_OVERHEAD + (element.length / 8) * 8;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    return BYTE_ARRAY_LIST_OVERHEAD + memberOverhead;
+  }
+
+  @Override
+  public int hashCode() {
+    final int PRIME_NUMBER = 31;
+    int hashCode = 1;
+    ListIterator<byte[]> iterator = this.listIterator();
+    while (iterator.hasNext()) {
+      int index = iterator.nextIndex() + 1;
+      hashCode = hashCode * (PRIME_NUMBER % index) + Arrays.hashCode(iterator.next());

Review comment:
       Consequence of incorrect test below, simplified.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -0,0 +1,119 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_LIST;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+
+import org.junit.Test;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.serialization.ByteArrayDataInput;
+import org.apache.geode.internal.serialization.SerializationContext;
+
+public class RedisListTest {
+
+  @Test
+  public void confirmSerializationIsStable() throws IOException, ClassNotFoundException {
+    RedisList list1 = createRedisList(1, 2);
+    int expirationTimestamp = 1000;
+    list1.setExpirationTimestampNoDelta(expirationTimestamp);
+    HeapDataOutputStream out = new HeapDataOutputStream(100);
+    DataSerializer.writeObject(list1, out);
+    ByteArrayDataInput in = new ByteArrayDataInput(out.toByteArray());
+    RedisList list2 = DataSerializer.readObject(in);
+    assertThat(list2.getExpirationTimestamp())
+        .isEqualTo(list1.getExpirationTimestamp())
+        .isEqualTo(expirationTimestamp);
+    assertThat(list2).isEqualTo(list1);
+  }
+
+  @Test
+  public void confirmToDataIsSynchronized() throws NoSuchMethodException {
+    assertThat(Modifier
+        .isSynchronized(RedisList.class
+            .getMethod("toData", DataOutput.class, SerializationContext.class).getModifiers()))
+                .isTrue();
+  }
+
+  @Test
+  public void hashcode_returnsSameValue_forEqualLists() {
+    RedisList list1 = createRedisList(1, 2);
+    RedisList list2 = createRedisList(1, 2);
+    assertThat(list1).isEqualTo(list2);
+    assertThat(list1.hashCode()).isEqualTo(list2.hashCode());
+  }
+
+  @Test
+  public void hashcode_returnsDifferentValue_forDifferentLists() {
+    RedisList list1 = createRedisList(1, 2);
+    RedisList list2 = createRedisList(2, 1);
+    assertThat(list1).isNotEqualTo(list2);
+    assertThat(list1.hashCode()).isEqualTo(list2.hashCode());

Review comment:
       Fixed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLLenIntegrationTest.java
##########
@@ -0,0 +1,124 @@
+/*
+ * 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_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+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 org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLLenIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  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 llen_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.llen("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void llen_givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LLEN, 1);
+  }
+
+  @Test
+  public void llen_givenNonexistentList_returnsZero() {
+    assertThat(jedis.llen("nonexistent")).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_returnsListLength() {
+    jedis.lpush(KEY, "e1", "e2", "e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(3L);
+
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e3");
+    assertThat(jedis.llen(KEY)).isEqualTo(2L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+    assertThat(jedis.llen(KEY)).isEqualTo(1L);
+
+    result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e1");
+    assertThat(jedis.llen(KEY)).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"one", "two", "three"};
+    String[] valuesToAdd = new String[] {"pear", "apple", "plum", "orange", "peach"};
+    jedis.lpush(KEY, valuesInitial);
+
+    final AtomicLong llenReference = new AtomicLong();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> llenReference.set(jedis.llen(KEY)))
+            .runWithAction(() -> {
+              AssertionsForClassTypes.assertThat(llenReference).satisfiesAnyOf(
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())
+                      .isEqualTo(valuesInitial.length),
+                  llenResult -> AssertionsForClassTypes.assertThat(llenResult.get())

Review comment:
       Fixed.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794943258



##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
##########
@@ -0,0 +1,72 @@
+/*
+ * 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.data.collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.geode.cache.util.ObjectSizer;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+
+public class SizeableByteArrayListTest {
+  private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+
+  @Before
+  public void setup() {}

Review comment:
       Removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/data/AbstractMemoryOverheadIntegrationTest.java
##########
@@ -239,7 +276,7 @@ private void measureAndCheckPerEntryOverhead(AddEntryFunction addEntry, Measurem
     // Put some entries to make sure we initialize any constant size data structures. We are
     // just trying to measure the cost of each add entry operation.
     for (int i = 0; i < WARM_UP_ENTRY_COUNT; i++) {
-      String uniqueString = String.format("warmup-%10d", i);
+      String uniqueString = String.format("warmup-%010d", i);

Review comment:
       It was deliberate, because it could produce string with spaces in them which confused the earlier version of the LPUSh executor. It's no longer necessary thanks to later development. Good catch!




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794952816



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;

Review comment:
       Oh, wait, I see what the problem is now. Instead of `assertThat(jedis.keys(KEY_WITH_TAG)).isEmpty();` we should be doing:
   ```
   assertThat(jedis.exists(KEY_WITH_TAG)).isFalse();
   ```
   This explicitly checks that the key we're interested in exists, and doesn't require a different key from the rest of the tests.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] DonalEvans commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r794952071



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
##########
@@ -0,0 +1,113 @@
+/*
+ * 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.internal.RedisConstants.ERROR_WRONG_TYPE;
+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.concurrent.atomic.AtomicLong;
+
+import org.assertj.core.api.AssertionsForClassTypes;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";
+  // TODO: make private when we implement Redis 6.2+ behavior for LPOP
+  public JedisCluster jedis;
+
+  @Before
+  public void setUp() {
+    jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()), REDIS_CLIENT_TIMEOUT);
+  }
+
+  @After
+  public void tearDown() {
+    flushAll();
+    jedis.close();
+  }
+
+  // not checking LPOP argument count here, see LPopIntegrationTest
+
+  @Test
+  public void lpop_withStringFails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.lpop("string")).hasMessageContaining(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void lpop_withNonExistentKey_returnsNull() {
+    assertThat(jedis.lpop("nonexistent")).isNull();
+  }
+
+  @Test
+  public void lpop_returnsOneMember() {
+    jedis.lpush(KEY, "e1", "e2");
+    String result = jedis.lpop(KEY);
+    assertThat(result).isEqualTo("e2");
+  }
+
+  @Test
+  public void lpop_removesKey_whenLastElementRemoved() {
+    final String KEY_WITH_TAG = "{tag}" + KEY;

Review comment:
       Weird, I wonder why the client is executing a KEYS command in this test. We're able to get away without hashtags in most other integration tests just fine.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1023422656


   This pull request **introduces 1 alert** when merging b0cdd9e8a7b251f409e01c0a8018f69733ab106f into bbe9a3acf2f0812ef733dfe74f07fb9412c886e3 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-bdc7e665bdce8b0f56e9c6d0fb4b572da498db1d)
   
   **new alerts:**
   
   * 1 for Inconsistent equals and hashCode


-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] jdeppe-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
jdeppe-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r785184404



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LLenExecutor.java
##########
@@ -0,0 +1,38 @@
+/*
+ * 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 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.ExecutionHandlerContext;
+
+public class LLenExecutor implements CommandExecutor {
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    int result = context.listLockedExecute(key, false,

Review comment:
       I think that his should be updating stats here. That thought leads me to point out that we're missing updates to `AbstractHitsMissesIntegrationTest`.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/netty/ExecutionHandlerContext.java
##########
@@ -483,17 +484,25 @@ private RedisSortedSet getRedisSortedSet(RedisKey key, boolean updateStats) {
     return getRegionProvider().getTypedRedisData(RedisDataType.REDIS_SORTED_SET, key, updateStats);
   }
 
-
   public <R> R setLockedExecute(RedisKey key, boolean updateStats,
       FailableFunction<RedisSet, R> function) {
     return getRegionProvider().lockedExecute(key,
         () -> function.apply(getRedisSet(key, updateStats)));
   }
 
+  public <R> R listLockedExecute(RedisKey key, boolean updateStats,
+      FailableFunction<RedisList, R> function) {
+    return getRegionProvider().lockedExecute(key,
+        () -> function.apply(getRedisList(key, updateStats)));
+  }
+
   public RedisSet getRedisSet(RedisKey key, boolean updateStats) {
     return getRegionProvider().getTypedRedisData(RedisDataType.REDIS_SET, key, updateStats);
   }
 
+  public RedisList getRedisList(RedisKey key, boolean updateStats) {

Review comment:
       This method can be private




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] jdeppe-pivotal commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
jdeppe-pivotal commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r785179030



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPopDUnitTest.java
##########
@@ -0,0 +1,155 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class LPopDUnitTest {

Review comment:
       I would definitely want to see tests that move buckets around and/or restart VMs. That is where issues with delta propagation would crop up.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788176562



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);

Review comment:
       It makes sense to have it own rather than subclass, especially because later on we're going to need to have the contents support equals() and such.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -0,0 +1,223 @@
+/*
+ * 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.data;
+
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.serialization.DeserializationContext;
+import org.apache.geode.internal.serialization.KnownVersion;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.redis.internal.data.collections.SizeableList;
+import org.apache.geode.redis.internal.data.delta.AddByteArrays;
+import org.apache.geode.redis.internal.data.delta.RemoveByteArrays;
+
+public class RedisList extends AbstractRedisData {
+  protected static final int REDIS_LIST_OVERHEAD = memoryOverhead(RedisList.class);
+  private ElementList elements;
+
+  public RedisList(Collection<byte[]> collection) {
+    this.elements = new ElementList(collection);
+  }
+
+  /**
+   * For deserialization only.
+   */
+  public RedisList() {}
+
+  /**
+   * @param elementsToAdd members to add to this set; NOTE this list may by
+   *        modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the number of members actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    for (byte[] element : elementsToAdd) {
+      prependElement(element);
+    }
+    int elementsAdded = elementsToAdd.size();
+    if (elementsAdded != 0) {
+      storeChanges(region, key, new AddByteArrays(elementsToAdd));
+    }
+    return elementsAdded;
+  }
+
+  /**
+   * @param region the region this instance is stored in
+   * @param key the name of the set to add to
+   * @return the element actually popped
+   */
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
+    int originalSize = elements.size();
+    if (originalSize == 0) {
+      return null;
+    }
+
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
+    }
+    return popped;
+  }
+
+  /**
+   * @return the number of elements in the list
+   */
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
+    return elements.size();
+  }
+
+  @Override
+  public void applyAddByteArrayDelta(byte[] bytes) {
+    prependElement(bytes);
+  }
+
+  @Override
+  public void applyRemoveByteArrayDelta(byte[] bytes) {
+    elementRemove(bytes);
+  }
+
+  /**
+   * Since GII (getInitialImage) can come in and call toData while other threads
+   * are modifying this object, the striped executor will not protect toData.
+   * So any methods that modify "members" needs to be thread safe with toData.
+   */
+
+  @Override
+  public synchronized void toData(DataOutput out, SerializationContext context) throws IOException {
+    super.toData(out, context);
+    DataSerializer.writePrimitiveInt(elements.size(), out);
+    for (byte[] member : elements) {
+      DataSerializer.writeByteArray(member, out);
+    }
+  }
+
+  @Override
+  public void fromData(DataInput in, DeserializationContext context)
+      throws IOException, ClassNotFoundException {
+    super.fromData(in, context);
+    int size = DataSerializer.readPrimitiveInt(in);
+    elements = new ElementList(Collections.emptyList()); // TODO: zero arg constructor!!
+    for (int i = 0; i < size; ++i) {
+      elements.add(DataSerializer.readByteArray(in));
+    }
+  }
+
+  @Override
+  public int getDSFID() {
+    return REDIS_LIST_ID;
+  }
+
+  @VisibleForTesting
+  synchronized boolean appendElement(byte[] elementToAdd) {
+    return elements.add(elementToAdd);
+  }
+
+  @VisibleForTesting
+  synchronized boolean prependElement(byte[] elementToAdd) {
+    elements.add(0, elementToAdd);
+    return true;
+  }
+
+  @VisibleForTesting
+  synchronized boolean elementRemove(byte[] memberToRemove) {
+    return elements.remove(memberToRemove);
+  }
+
+  @Override
+  public RedisDataType getType() {
+    return REDIS_LIST;
+  }
+
+  @Override
+  protected boolean removeFromRegion() {
+    return elements.isEmpty();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof RedisList)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    RedisList redisList = (RedisList) o;
+
+    if (redisList.elements.size() != elements.size()) {

Review comment:
       Done, see RedisElementList.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] ringles commented on a change in pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
ringles commented on a change in pull request #7261:
URL: https://github.com/apache/geode/pull/7261#discussion_r788869335



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.assertAtLeastNArgs;
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.RedisIntegrationTest;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+
+public abstract class AbstractListsIntegrationTest implements RedisIntegrationTest {

Review comment:
       Split up.




-- 
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-unsubscribe@geode.apache.org

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



[GitHub] [geode] lgtm-com[bot] commented on pull request #7261: Geode 9892 create infrastructure for redis lists

Posted by GitBox <gi...@apache.org>.
lgtm-com[bot] commented on pull request #7261:
URL: https://github.com/apache/geode/pull/7261#issuecomment-1017931332


   This pull request **introduces 1 alert** when merging a079fcd920c26bd77fe281b75df964dc893ecb03 into da6008a4f53aac3d20adb0198f880a70d6b3ce99 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-73eb26aefbbd1afa4b9370f38b2855eb0cfcacc7)
   
   **new alerts:**
   
   * 1 for Equals or hashCode on arrays


-- 
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-unsubscribe@geode.apache.org

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