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/02/28 20:36:21 UTC

[GitHub] [geode] ringles opened a new pull request #7403: GEODE-9953: Implement LTRIM Command

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


   <!-- Thank you for submitting a contribution to Apache Geode. -->
   
   
   - [ ] Is there a JIRA ticket associated with this PR? Is it referenced in the commit message?
   
   - [ ] Has your PR been rebased against the latest commit within the target branch (typically `develop`)?
   
   - [ ] Is your initial contribution a single, squashed commit?
   
   - [ ] Does `gradlew build` run cleanly?
   
   - [ ] Have you written or updated unit tests to verify your changes?
   
   - [ ] If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under [ASF 2.0](http://www.apache.org/legal/resolved.html#category-a)?
   
   <!-- Note:
   Please ensure that once the PR is submitted, check Concourse for build issues and
   submit an update to your PR as soon as possible. If you need help, please send an
   email to dev@geode.apache.org.
   -->
   


-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }

Review comment:
       Checking this in tests 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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      if (boundedEnd < length) {
+        // trim stuff at end of list
+        elementList.subList(boundedEnd + 1, length).clear();

Review comment:
       Fixed, 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] jdeppe-pivotal commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -82,28 +92,93 @@ public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region
    */
   public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
     byte[] popped = elementRemove(0);
-    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex(version);
     removed.add(0);
     storeChanges(region, key, removed);
     return popped;
   }
 
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this set; NOTE this list may be 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 int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    elementsPush(elementsToAdd);
+    storeChanges(region, key, new AddByteArraysVersioned(version, elementsToAdd));
     return elementList.size();
   }
 
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int preservedVersion;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      preservedVersion = version;
+    }
+    System.out.println(
+        "DEBUG ltrim, preserved version:" + preservedVersion + " (version:" + version + ")");
+    storeChanges(region, key, new RemoveElementsByIndex(preservedVersion, removed));
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, -1);
+    }
+  }
+
   @Override
-  public void applyAddByteArrayDelta(byte[] bytes) {
-    elementPush(bytes);
+  public void applyAddByteArrayVersionedDelta(int version, byte[] bytes) {
+    System.out.println("DEBUG abav: local version:" + this.version + " incoming:" + version);
+    if (version != this.version) {
+      elementPush(bytes);
+    }
+    this.version = version;
   }
 
   @Override
-  public void applyRemoveElementsByIndex(List<Integer> indexes) {
-    for (int index : indexes) {
-      elementRemove(index);
+  public void applyRemoveElementsByIndex(int version, List<Integer> indexes) {
+    System.out.println("DEBUG arebi: local version:" + this.version + " incoming:" + version);
+    synchronized (this) {
+      if (version != this.version) {
+        elementsRemove(indexes);
+      }
+      this.version = version;

Review comment:
       There shouldn't be any need to synchronize here since this is a delta application and there will be locking at the Geode level to protect the entry. I think this should just be:
   ```
   if (version > this.version) {
     elementsRemove(indexes);
     this.version = version;
   }
   ```
   You want to update the version to match the change being applied.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean isRunning = new AtomicBoolean(true);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), isRunning, elementList1);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), isRunning, elementList2);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(keys.get(2), isRunning, elementList3);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    isRunning.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key,
+      AtomicBoolean isRunning,
+      List<String> elementList) {
+    while (isRunning.get()) {
+      lpushPerformAndVerify(key, elementList);
+
+      for (int i = 1; i <= INITIAL_LIST_SIZE / 2 && isRunning.get(); i++) {
+        long lastIndex = jedis.llen(key) - 2;
+        try {
+          assertThat(jedis.lindex(key, 0)).isEqualTo(makeElementString(key, INITIAL_LIST_SIZE - i));
+          assertThat(jedis.lindex(key, lastIndex)).isEqualTo(makeElementString(key, i));
+          jedis.ltrim(key, 1, lastIndex);
+          assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastIndex);
+        } catch (Exception ex) {
+          isRunning.set(false); // test is over
+          throw new RuntimeException("Exception performing LTRIM for list '"
+              + key + "' at step " + i + ": " + ex.getMessage());
+        }
+      }

Review comment:
       Now I get what you mean by the throwable comment earlier. 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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,56 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   */
+  public Void ltrim(int start, int end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      elementsRetainByIndexRange(boundedStart, boundedEnd);
+
+      retainElementsByRange =
+          new RetainElementsByIndexRange(incrementAndGetVersion(), boundedStart, boundedEnd);
+    }
+    storeChanges(region, key, retainElementsByRange);
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {

Review comment:
       `index` in these two methods is always an `int` so the method signatures should be updated to reflect this and then the casts to `int` in the method can be 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] ringles commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+    RemoveElementsByIndex removeElementsByIndex;
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        // Remove any elements before boundedStart
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      newVersion = incrementAndGetVersion();
+      removeElementsByIndex = new RemoveElementsByIndex(newVersion, removed);
+    }
+    storeChanges(region, key, removeElementsByIndex);
+    return null;

Review comment:
       listLockedExecuted wants a return value, it doesn't take void(). I suppose we could add one like 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] ringles commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);

Review comment:
       Rebase mooted this.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);

Review comment:
       Outdated now.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list to add to

Review comment:
       Fixed

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
##########
@@ -0,0 +1,57 @@
+/*
+ * 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_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      byte[] startI = commandElems.get(startIndex);
+      byte[] stopI = commandElems.get(stopIndex);
+      start = Coder.bytesToLong(startI);
+      end = Coder.bytesToLong(stopI);
+    } catch (NumberFormatException e) {
+      return RedisResponse.error(ERROR_NOT_INTEGER);
+    }
+
+    byte[] retVal =
+        context.listLockedExecute(key, false, list -> list.ltrim(start, end, region, key));
+    // return RedisResponse.error(ERROR_NOT_INTEGER);

Review comment:
       Yanked.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -356,10 +432,32 @@ protected synchronized void elementsPushHead(List<byte[]> elementsToAdd) {
     }
   }
 
+  public synchronized void elementsRemove(List<Integer> indexList) {
+    for (Integer element : indexList) {
+      elementList.remove(element.intValue());
+    }
+  }
+
   public synchronized void elementReplace(int index, byte[] newValue) {
     elementList.set(index, newValue);
   }
 
+  public synchronized void elementsRetainByIndexRange(int start, int end) {
+    if (start < 0) {
+      // Remove everything
+      elementList.clear();
+      return;
+    }

Review comment:
       Removed, tests still pass.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -356,10 +432,32 @@ protected synchronized void elementsPushHead(List<byte[]> elementsToAdd) {
     }
   }
 
+  public synchronized void elementsRemove(List<Integer> indexList) {
+    for (Integer element : indexList) {
+      elementList.remove(element.intValue());
+    }
+  }
+
   public synchronized void elementReplace(int index, byte[] newValue) {
     elementList.set(index, newValue);
   }
 
+  public synchronized void elementsRetainByIndexRange(int start, int end) {
+    if (start < 0) {
+      // Remove everything
+      elementList.clear();
+      return;
+    }
+
+    if (end < elementList.size()) {
+      elementList.subList(end + 1, elementList.size()).clear();
+    }
+
+    if (start > 0) {
+      elementList.subList(0, start).clear();
+    }

Review comment:
       When a delta comes in, we may remove things at the beginning of the list, or the end, or both. They are separate calls to subList().clear(), and I'm trying to avoid calling them if there's no need. If the delta values are invalid, versioning is _supposed_ to catch that...

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
##########
@@ -46,6 +46,15 @@ public void add(int index) {
     indexes.add(index);
   }
 
+  public RemoveElementsByIndex(byte version, List<Integer> indexes) {
+    super(version);
+    this.indexes = indexes;
+  }
+
+  public int size() {
+    return this.indexes.size();
+  }
+

Review comment:
       Leftovers from an earlier implementation. Yanked.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {

Review comment:
       OK

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {

Review comment:
       Parameterized, new cases added.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);

Review comment:
       Updated.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");

Review comment:
       At one point early in dev, it helped. Good catch.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();

Review comment:
       Yanked.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      if (boundedEnd < length) {
+        // trim stuff at end of list
+        elementList.subList(boundedEnd + 1, length).clear();

Review comment:
       shouldn't this use elementsRetainByIndexRange like we do when applying the delta so that the memoryOverhead will be correctly updated on the primary?




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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


   This pull request **fixes 1 alert** when merging 69ef004b28cc956d9eec296a863f8a08d2e65a3e into f3b9636b7af7ac733de640ceeea50359075ad6f4 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-256009a88488373b0878d6f7f7bd5fa96ba6ea14)
   
   **fixed 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 #7403: GEODE-9953: Implement LTRIM Command

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


   This pull request **fixes 1 alert** when merging fc2fd5253445b89382acc668b8de8194b2082fbc into 5dce03c9136e66d2497fdcbad8943620442d956b - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-1668df60ad262bdd52b1f731902fb754f556f25e)
   
   **fixed 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] DonalEvans commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -136,15 +138,15 @@ public void shouldNotLoseData_givenPrimaryServerCrashesDuringOperations()
     long length;
     for (String key : keys) {
       length = jedis.llen(key);
-      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      assertThat(length).isCloseTo(ITERATION_COUNT * 2 * PUSH_LIST_SIZE, within(6L));
       assertThat(length % 3).isEqualTo(0);
       validateListContents(key, length, keyToElementListMap);
     }
   }
 
   private void lpushPerformAndVerify(String key, List<String> elementList,
       AtomicLong runningCount) {
-    for (int i = 0; i < MINIMUM_ITERATIONS; i++) {
+    for (int i = 0; i < ITERATION_COUNT; i++) {

Review comment:
       Rather than having this test always do a fixed number of LPUSH operations and moving buckets while they're ongoing, it would be better to have a fixed number of bucket moves performed (25 seems like a reasonable number) and have LPUSH happening continuously until those bucket moves are finished, since what we really want to test here is how LPUSH behaves when we move buckets, so we should make sure that we always do the same number of bucket moves.
   
   This can be achieved by replacing the for loop in this method with a while loop, having an `AtomicBoolean` rather than an `AtomicInteger` as the condition (named something like continueRunning maybe), and setting that `AtomicBoolean` to false after the for loop that moves buckets completes. A similar approach is used in `LInsertDUnitTest`.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.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.internal.RedisConstants.ERROR_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();

Review comment:
       Minor performance improvement, but these lines should be moved to below the try/catch as it's possible we return from this method before needing them.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,

Review comment:
       This method does not need to return a byte array, as the LTRIM command just returns "OK" if it succeeds. This method should therefore be `public Void`, returning `null` unless an exception is encountered (we can't have it be lower-case v `void` because the `listLockedExecute()` method expects a function, which can't return `void` even though in this case we want to).
   
   If all of the comments for this method are taken on board, it would end up looking something like:
   ```
     public Void ltrim(long start, long end, Region<RedisKey, RedisData> region,
         RedisKey key) {
       int length = elementList.size();
       int boundedStart = getBoundedStartIndex(start, length);
       int boundedEnd = getBoundedEndIndex(end, length);
   
       if (boundedStart > boundedEnd || boundedStart == length) {
         // Remove everything
         region.remove(key);
         return null;
       }
   
       if (boundedStart == 0 && boundedEnd == length) {
         // No-op, return without modifying the list
         return null;
       }
   
       RetainElementsByIndexRange retainElementsByRange;
       synchronized (this) {
         if (boundedEnd < length) {
           // trim stuff at end of list
           elementList.subList(boundedEnd + 1, length).clear();
         }
         if (boundedStart > 0) {
           // trim stuff at start of list
           elementList.subList(0, boundedStart).clear();
         }
         retainElementsByRange =
             new RetainElementsByIndexRange(incrementAndGetVersion(), boundedStart, boundedEnd);
       }
       storeChanges(region, key, retainElementsByRange);
       return null;
     }
   ```

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
##########
@@ -46,6 +46,15 @@ public void add(int index) {
     indexes.add(index);
   }
 
+  public RemoveElementsByIndex(byte version, List<Integer> indexes) {
+    super(version);
+    this.indexes = indexes;
+  }
+
+  public int size() {
+    return this.indexes.size();
+  }
+

Review comment:
       Why was these methods added? They're not being called anywhere.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {

Review comment:
       Rather than one big test with multiple cases, it would be better to break this up into individual tests, especially since the contents of the list is being reset between each call to LTRIM. To reduce duplication, it might be worth turning this into a parameterized test, along with the one below it. That way you could have one test that tests all of the possible combinations of start and end that produce non-empty ranges and one test that tests all the combinations that produce empty ranges.
   
   In particular it would be good to test with values for the end index that are equal to the size of the list, as that case is currently missing.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);

Review comment:
       To make this test a bit more robust, it would be good to select start and end indexes such that we remove some elements from the start and some elements form the end of the list. That way we're checking that we're using both start and end indexes correctly in the Delta that we send.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();
+  }
+
+  @Test
+  public void withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "trois"};
+    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 -> jedis.ltrim(KEY, 0, 2),
+        i -> lpopReference.set(jedis.lpop(KEY)))
+            .runWithAction(() -> {
+              assertThat(lpopReference).satisfiesAnyOf(
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("orange"),
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("troix"));
+              jedis.del(KEY);
+              jedis.lpush(KEY, valuesInitial);
+            });

Review comment:
       The call to LPOP here should be removed, since we're not trying to test the concurrent behaviour of LPUSH, LTRIM *and* LPOP. With the test as it is, we could do the push, then do the pop, then do the trim, which is not what we're trying to test.
   
   Instead, we should be calling LRANGE in the `runWithAction` lambda and asserting that the contents of the list is either `{"un", "deux", "trois"}` if the LPUSH happened first or `{"plum", "peach", "orange"}` if the LTRIM happened first (but with the elements in reverse order, because LPUSH likes to complicate things).

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.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.internal.RedisConstants.ERROR_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      start = Coder.bytesToLong(commandElems.get(startIndex));
+      end = Coder.bytesToLong(commandElems.get(stopIndex));

Review comment:
       Having these values be `long` here but then casting them to `int` inside the `ltrim()` method could potentially lead to overflows, so it might be safer to wrap these calls to `bytesToLong()` in `narrowLongToInt()` to safely convert from `long` to `int` and have the values be `int` throughout.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -356,10 +432,32 @@ protected synchronized void elementsPushHead(List<byte[]> elementsToAdd) {
     }
   }
 
+  public synchronized void elementsRemove(List<Integer> indexList) {
+    for (Integer element : indexList) {
+      elementList.remove(element.intValue());
+    }
+  }
+
   public synchronized void elementReplace(int index, byte[] newValue) {
     elementList.set(index, newValue);
   }
 
+  public synchronized void elementsRetainByIndexRange(int start, int end) {
+    if (start < 0) {
+      // Remove everything
+      elementList.clear();
+      return;
+    }
+
+    if (end < elementList.size()) {
+      elementList.subList(end + 1, elementList.size()).clear();
+    }
+
+    if (start > 0) {
+      elementList.subList(0, start).clear();
+    }

Review comment:
       I think it might be better to not have the checks on the values of `end` and `start` here, since if in the time between us sending the delta with valid values and the delta arriving at the secondary, those values have become invalid, we should probably throw an exception rather than just silently hide the fact, since we really don't expect that to ever happen.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int retainStart = 0;
+    int retainEnd = length;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    RetainElementsByIndexRange retainElementsByRange;
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      retainStart = -1;

Review comment:
       If we know we're going to remove everything, we should be able to do that here, by just calling `region.remove(key)` followed by returning null. This avoids the need to muck around with deltas or synchronization. We can also return early here if the range to be retained encompasses the entire list, i.e. `boundedStart == 0 && boundedEnd = length` since then we know we're not going to modify the list at all and can just return immediately.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int retainStart = 0;
+    int retainEnd = length;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    RetainElementsByIndexRange retainElementsByRange;
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      retainStart = -1;
+    } else {
+      if (boundedStart > retainStart) {
+        retainStart = boundedStart;
+      }
+      if (boundedEnd <= retainEnd) {
+        retainEnd = boundedEnd;
+      }

Review comment:
       Given the limits put on `boundedStart` and `boundedEnd` by the `getBounded***Index()` methods, I'm not sure I see the purpose of the `retainStart` and `retainEnd` variables here. When we get to this block, `boundedStart` is always either 0 or greater, meaning that in either case, `retainStart` will have the same value as `boundedStart`. Likewise, there's no situation in which `boundedEnd` is not less than or equal to `length` (the initial value of `retainEnd`) so `retainEnd` will always have the same value as `boundedEnd`. I think it would simplify things a bit to just use `boundedStart` and `boundedEnd` throughout and remove the `retainStart` and `retainEnd` variables.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -356,10 +432,32 @@ protected synchronized void elementsPushHead(List<byte[]> elementsToAdd) {
     }
   }
 
+  public synchronized void elementsRemove(List<Integer> indexList) {
+    for (Integer element : indexList) {
+      elementList.remove(element.intValue());
+    }
+  }
+
   public synchronized void elementReplace(int index, byte[] newValue) {
     elementList.set(index, newValue);
   }
 
+  public synchronized void elementsRetainByIndexRange(int start, int end) {
+    if (start < 0) {
+      // Remove everything
+      elementList.clear();
+      return;
+    }

Review comment:
       We should not need to handle this case in the Delta, since if all elements were removed from the list, the list should have just been removed from the region, so there shouldn't be anything to apply the delta to.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -258,6 +316,22 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     return popped;
   }
 
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, -1);

Review comment:
       I think this is incorrect, as the end index provided by the user is inclusive, so an index of `-1` should be equivalent to an index of `size`, instead of `size - 1`, which is what would be returned here. For contrast, if the user directly passed `size` then this method would return `size` instead of `size -1`.
   
   We need to be careful with how we handle inclusive vs exclusive indexes here, since Java uses an exclusive index for the second argument in methods like `sublist()`, meaning that if start == end then the list is empty, but Redis uses an inclusive index for end, meaning that if start == end, then list has size 1. Either this method should return the inclusive end index (and be named to relfect that, since it's non-standard in Java) or it should always return the exclusive end index and the logic in methods that call it should be adjusted to ensure that we're not behaving incorrectly.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -155,11 +157,13 @@ private void lpushPerformAndVerify(String key, List<String> elementList,
 
   private void validateListContents(String key, long length,
       HashMap<String, List<String>> keyToElementListMap) {
-    while (jedis.llen(key) > 0) {
+    length = jedis.llen(key);
+    while (length > 0) {
       List<String> elementList = keyToElementListMap.get(key);
       assertThat(jedis.lpop(key)).isEqualTo(elementList.get(2));
       assertThat(jedis.lpop(key)).isEqualTo(elementList.get(1));
       assertThat(jedis.lpop(key)).isEqualTo(elementList.get(0));
+      length = jedis.llen(key);

Review comment:
       This change seems unnecessary. It would probably be better to just remove `length` from the method signature and keep the while loop as it was.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {

Review comment:
       This test name should be "returnsOK" instead of "returnsNull"

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);

Review comment:
       -4 is inside the list size range, corresponding to index 0; a value of -5 would be required for a start index that is (effectively) negative.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {

Review comment:
       This test name might be better as something like "removesKey_whenAllElementsTrimmed"

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RetainElementsByIndexRange.java
##########
@@ -0,0 +1,52 @@
+/*
+ * 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.readPrimitiveInt;
+import static org.apache.geode.redis.internal.data.delta.DeltaType.RETAIN_ELEMENTS_BY_INDEX_RANGE;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.redis.internal.data.AbstractRedisData;
+
+public class RetainElementsByIndexRange extends DeltaInfo {
+  private int start;
+  private int end;

Review comment:
       These can be `final`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");

Review comment:
       A tag is not necessary for this test, so these lines can be replaced with a call to `initializeTestList()`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();

Review comment:
       It's not necessary to call LLEN here in addition to checking if the key exists. Doing so is just testing the behaviour of LLEN rather than checking that the key is correctly removed from the region.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);

Review comment:
       This test would be better if instead of running until we've done a certain number of LTRIM calls, we instead do constant LTRIM calls until we've done a certain number of bucket moves. The LRemDUnitTest that does bucket moves that's being added [as part of the LREM PR](https://github.com/apache/geode/pull/7431/files) is a great example of how we want to be approaching these sort of tests, I think, so it could be helpful to use it as a guide.
   
   The basic idea is that we do our operation constantly until we're finished moving buckets, but since LTRIM removes elements, we need to reset the list to its initial state once it's been emptied, then continue calling LTRIM. That way we can be sure that we do enough bucket moves to make the test consistent and useful, without having to worry that we run out of elements to trim.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(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 < 100 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    runningCount.set(0);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key, AtomicLong runningCount) {
+    assertThat(jedis.llen(key)).isEqualTo(INITIAL_LIST_SIZE);
+
+    int lastElementIndex = INITIAL_LIST_SIZE - 1;
+    int i = 0;
+    while (jedis.llen(key) > 1 && runningCount.get() > 0) {
+      String expected = makeElementString(key, i);
+      try {
+        assertThat(jedis.lindex(key, lastElementIndex)).isEqualTo(expected);
+        jedis.ltrim(key, 0, lastElementIndex - 1);
+        assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastElementIndex);
+        lastElementIndex--;

Review comment:
       Could we choose indexes for start and end here so that we're keeping some middle part of the list instead of just trimming the last element? That way we make sure that we're exercising the whole of the Delta that we send, not just the end index. Maybe just trim the first and last elements?
   
   Also, it would be good to assert on the entire contents of the list (using LRANGE) rather than just the size, as we want to make sure that we're not removing the wrong elements.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved_multipleTimes() {

Review comment:
       I'm not super clear on what this test is showing, since we already have a test that shows that the key is removed if all elements are trimmed, and a test that shows the behaviour when LTRIM is called on a non-existent key. I think this can probably just be removed.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -114,7 +115,8 @@ public void givenBucketsMovedDuringLPush_elementsAreAddedAtomically()
 
     for (String key : keys) {
       long length = jedis.llen(key);
-      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      assertThat(length).isCloseTo(ITERATION_COUNT * 2 * PUSH_LIST_SIZE, within(6L));
+      assertThat(length % 3).isEqualTo(0);

Review comment:
       Agreed, this assertion should not be using `isCloseTo()` since we should not be seeing duplicated commands 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] dschneider-pivotal commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list to add to

Review comment:
       change "add to" to "pop from"

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+    RemoveElementsByIndex removeElementsByIndex;
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        // Remove any elements before boundedStart
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {

Review comment:
       no need for this size check. The for statement in elementsRemove will do nothing if remove is empty

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);

Review comment:
       I think the sync can end right after the removeFirstElement call. No need to hold sync while creating the DeltaInfo instance

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();

Review comment:
       It does not seem performant to build up an ArrayList of every index we are going to remove. All you need for this DeltaInfo is two numbers, start and end. So just add an RetainElementsByIndexRange DeltaInfo.
   You can make sure they are actual "bounded" indexes.
   Then instead of elementsRemove having to iterate over the list for very item you are removing, you can just implement elementsRetainByIndexRange that takes two ints. You should be able to implement that method by using an Iterator that only traverse the linked list once.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
##########
@@ -0,0 +1,57 @@
+/*
+ * 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_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      byte[] startI = commandElems.get(startIndex);
+      byte[] stopI = commandElems.get(stopIndex);
+      start = Coder.bytesToLong(startI);
+      end = Coder.bytesToLong(stopI);
+    } catch (NumberFormatException e) {
+      return RedisResponse.error(ERROR_NOT_INTEGER);
+    }
+
+    byte[] retVal =
+        context.listLockedExecute(key, false, list -> list.ltrim(start, end, region, key));
+    // return RedisResponse.error(ERROR_NOT_INTEGER);

Review comment:
       remove this comment

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -422,16 +424,21 @@ synchronized boolean membersRemove(byte[] memberToRemove) {
    * @return the number of members actually added
    */
   public long sadd(List<byte[]> membersToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
-    AddByteArrays delta = new AddByteArrays();
+    AddByteArrays delta;
+    byte newVersion;
     int membersAdded = 0;
-    for (byte[] member : membersToAdd) {
-      if (membersAdd(member)) {
-        delta.add(member);
-        membersAdded++;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();

Review comment:
       newVersion can be declared inside the sync

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);

Review comment:
       no need to hold sync while creating AddByteArray

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -422,16 +424,21 @@ synchronized boolean membersRemove(byte[] memberToRemove) {
    * @return the number of members actually added
    */
   public long sadd(List<byte[]> membersToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
-    AddByteArrays delta = new AddByteArrays();
+    AddByteArrays delta;
+    byte newVersion;
     int membersAdded = 0;
-    for (byte[] member : membersToAdd) {
-      if (membersAdd(member)) {
-        delta.add(member);
-        membersAdded++;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      delta = new AddByteArrays(newVersion);
+      for (byte[] member : membersToAdd) {
+        if (membersAdd(member)) {
+          delta.add(member);
+          membersAdded++;
+        }
+      }
+      if (membersAdded == 0) {

Review comment:
       The sync can end before you check if membersAdded is 0

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+    RemoveElementsByIndex removeElementsByIndex;
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        // Remove any elements before boundedStart
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      newVersion = incrementAndGetVersion();
+      removeElementsByIndex = new RemoveElementsByIndex(newVersion, removed);
+    }
+    storeChanges(region, key, removeElementsByIndex);
+    return null;

Review comment:
       It seems like this method return type should be void since it always returns null.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;

Review comment:
       Couldn't this just call lpush(elementsToAdd, region, key) instead of duplicating the code?

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+    RemoveElementsByIndex removeElementsByIndex;
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        // Remove any elements before boundedStart
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      newVersion = incrementAndGetVersion();

Review comment:
       The sync only needs to be around elementsRemove and incrementAndGetVersion. No need to hold it while populating the removed list. Remember we have no other writers. Just a possible concurrent reader calling toData

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);

Review comment:
       no need to hold sync while creating AddByteArray




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;

Review comment:
       Mooted by rebase.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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


   This pull request **fixes 1 alert** when merging 18c118ac7cbe65f6d15953218fc684470f1779c2 into 41844a5128f94cba6e27a1fdb0184db35a3f36ac - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-59e7439ff9638bf540d94a1d7d4bfdbb91226cb9)
   
   **fixed 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] jdeppe-pivotal commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -82,28 +92,93 @@ public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region
    */
   public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
     byte[] popped = elementRemove(0);
-    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex(version);
     removed.add(0);
     storeChanges(region, key, removed);
     return popped;
   }
 
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this set; NOTE this list may be 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 int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    elementsPush(elementsToAdd);
+    storeChanges(region, key, new AddByteArraysVersioned(version, elementsToAdd));
     return elementList.size();
   }
 
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int preservedVersion;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      preservedVersion = version;
+    }
+    System.out.println(
+        "DEBUG ltrim, preserved version:" + preservedVersion + " (version:" + version + ")");
+    storeChanges(region, key, new RemoveElementsByIndex(preservedVersion, removed));
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, -1);
+    }
+  }
+
   @Override
-  public void applyAddByteArrayDelta(byte[] bytes) {
-    elementPush(bytes);
+  public void applyAddByteArrayVersionedDelta(int version, byte[] bytes) {
+    System.out.println("DEBUG abav: local version:" + this.version + " incoming:" + version);
+    if (version != this.version) {
+      elementPush(bytes);
+    }
+    this.version = version;
   }
 
   @Override
-  public void applyRemoveElementsByIndex(List<Integer> indexes) {
-    for (int index : indexes) {
-      elementRemove(index);
+  public void applyRemoveElementsByIndex(int version, List<Integer> indexes) {
+    System.out.println("DEBUG arebi: local version:" + this.version + " incoming:" + version);
+    synchronized (this) {
+      if (version != this.version) {
+        elementsRemove(indexes);
+      }
+      this.version = version;

Review comment:
       There shouldn't be any need to synchronize here since this is a delta application and there will be locking at the Geode level to protect the entry. I think this should just be:
   ```
   if (version > this.version) {
     elementsRemove(indexes);
     this.version = version;
   }
   ```
   You want to update the version to match the change being applied. Similarly in `applyAddByteArrayVersionedDelta`.




-- 
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] nonbinaryprogrammer commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,228 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();

Review comment:
       I may be wrong, but I don't think it's necessary to do this method call between each of your test cases. You never actually modify the list

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,228 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();

Review comment:
       I think you can just call `assertThat(jedis.exists(keyWithTagForKeysCommand)).isTrue()`. Doesn't make a big difference but I think it's nicer for readability. The next test has this too.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
##########
@@ -0,0 +1,57 @@
+/*
+ * 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_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      byte[] startI = commandElems.get(startIndex);
+      byte[] stopI = commandElems.get(stopIndex);
+      start = Coder.bytesToLong(startI);
+      end = Coder.bytesToLong(stopI);
+    } catch (NumberFormatException e) {
+      return RedisResponse.error(ERROR_NOT_INTEGER);
+    }
+
+    byte[] retVal =
+        context.listLockedExecute(key, false, list -> list.ltrim(start, end, region, key));
+    // return RedisResponse.error(ERROR_NOT_INTEGER);

Review comment:
       oops dead code

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -359,8 +359,10 @@ public int scard() {
   }
 
   @Override
-  public void applyAddByteArrayDelta(byte[] bytes) {
-    membersAdd(bytes);
+  public void applyAddByteArrayDelta(List<byte[]> byteArrays) {

Review comment:
       this should be named `applyAddByteArraysDelta`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,228 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "troix"};

Review comment:
       "troix" is spelled "trois"

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,

Review comment:
       Please add javadocs for this method

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
##########
@@ -0,0 +1,57 @@
+/*
+ * 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_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      byte[] startI = commandElems.get(startIndex);
+      byte[] stopI = commandElems.get(stopIndex);
+      start = Coder.bytesToLong(startI);
+      end = Coder.bytesToLong(stopI);

Review comment:
       I don't see any reason to do this in two steps
   `start = Coder.bytesToLong(commandElems.get(startIndex));`




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {

Review comment:
       Rechristened.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved_multipleTimes() {

Review comment:
       Yeah, really only there during development for TDD.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,222 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+
+    assertThat(jedis.exists(keyWithTagForKeysCommand)).isFalse();
+  }
+
+  @Test
+  public void withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "trois"};
+    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 -> jedis.ltrim(KEY, 0, 2),
+        i -> lpopReference.set(jedis.lpop(KEY)))
+            .runWithAction(() -> {
+              assertThat(lpopReference).satisfiesAnyOf(
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("orange"),
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("troix"));
+              jedis.del(KEY);
+              jedis.lpush(KEY, valuesInitial);
+            });

Review comment:
       When the test was written, we didn't have LRANGE. Reworked.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);

Review comment:
       Significantly reworked.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);

Review comment:
       Reworked

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(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 < 100 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    runningCount.set(0);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key, AtomicLong runningCount) {
+    assertThat(jedis.llen(key)).isEqualTo(INITIAL_LIST_SIZE);
+
+    int lastElementIndex = INITIAL_LIST_SIZE - 1;
+    int i = 0;
+    while (jedis.llen(key) > 1 && runningCount.get() > 0) {
+      String expected = makeElementString(key, i);
+      try {
+        assertThat(jedis.lindex(key, lastElementIndex)).isEqualTo(expected);
+        jedis.ltrim(key, 0, lastElementIndex - 1);
+        assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastElementIndex);
+        lastElementIndex--;

Review comment:
       Got a test that removes the first and last each step, and verifies the values along the way. (Didn't have LRANGE when the test was first written.)




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.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.internal.RedisConstants.ERROR_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      start = Coder.bytesToLong(commandElems.get(startIndex));
+      end = Coder.bytesToLong(commandElems.get(stopIndex));

Review comment:
       Demoted.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.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.internal.RedisConstants.ERROR_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();

Review comment:
       Minorly updated.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int retainStart = 0;
+    int retainEnd = length;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    RetainElementsByIndexRange retainElementsByRange;
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      retainStart = -1;

Review comment:
       Simplified.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,

Review comment:
       Updated.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int retainStart = 0;
+    int retainEnd = length;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    RetainElementsByIndexRange retainElementsByRange;
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      retainStart = -1;
+    } else {
+      if (boundedStart > retainStart) {
+        retainStart = boundedStart;
+      }
+      if (boundedEnd <= retainEnd) {
+        retainEnd = boundedEnd;
+      }

Review comment:
       Eliminated.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+    RemoveElementsByIndex removeElementsByIndex;
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        // Remove any elements before boundedStart
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {

Review comment:
       Mooted by redesign.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);

Review comment:
       Reworked.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean isRunning = new AtomicBoolean(true);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), isRunning, elementList1);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), isRunning, elementList2);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(keys.get(2), isRunning, elementList3);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    isRunning.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key,
+      AtomicBoolean isRunning,
+      List<String> elementList) {
+    while (isRunning.get()) {
+      lpushPerformAndVerify(key, elementList);
+
+      for (int i = 1; i <= INITIAL_LIST_SIZE / 2 && isRunning.get(); i++) {
+        long lastIndex = jedis.llen(key) - 2;
+        try {
+          assertThat(jedis.lindex(key, 0)).isEqualTo(makeElementString(key, INITIAL_LIST_SIZE - i));
+          assertThat(jedis.lindex(key, lastIndex)).isEqualTo(makeElementString(key, i));
+          jedis.ltrim(key, 1, lastIndex);
+          assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastIndex);
+        } catch (Exception ex) {
+          isRunning.set(false); // test is over
+          throw new RuntimeException("Exception performing LTRIM for list '"
+              + key + "' at step " + i + ": " + ex.getMessage());
+        }
+      }

Review comment:
       Exceptions thrown by the `assert`s won't be caught by the `catch` block and so won't stop the test. Not critical, but you could change this to:
   ```
           try {
             assertThat(jedis.lindex(key, 0))
                 .as("lpush head verification failed at iteration " + i)
                 .isEqualTo(makeElementString(key, INITIAL_LIST_SIZE - i));
             assertThat(jedis.lindex(key, lastIndex))
                 .as("lpush tail verification failed at iteration " + i)
                 .isEqualTo(makeElementString(key, i));
             jedis.ltrim(key, 1, lastIndex);
             assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastIndex);
           } catch (Throwable ex) {
             isRunning.set(false); // test is over
             throw ex;
           }
   ```




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      if (boundedEnd < length) {
+        // trim stuff at end of list
+        elementList.clearSublist(boundedEnd + 1, length);
+      }
+      if (boundedStart > 0) {
+        // trim stuff at start of list
+        elementList.clearSublist(0, boundedStart);
+      }

Review comment:
       To prevent code duplication, this should probably be replaced with `elementsRetainByIndexRange(boundedStart, boundedEnd);`

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;

Review comment:
       It might be worth adding an implementation of `ltrim()` to `NullRedisList` that just returns immediately, since then we can avoid doing these steps. I'm not sure how expensive a call to `region.remove()` for a non-existent key is, but it's probably worth avoiding if we can.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      if (boundedEnd < length) {
+        // trim stuff at end of list
+        elementList.clearSublist(boundedEnd + 1, length);
+      }
+      if (boundedStart > 0) {
+        // trim stuff at start of list
+        elementList.clearSublist(0, boundedStart);
+      }
+      retainElementsByRange =
+          new RetainElementsByIndexRange(incrementAndGetVersion(), boundedStart, boundedEnd);
+    }
+    storeChanges(region, key, retainElementsByRange);
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, -1);
+    }

Review comment:
       This method is still inconsistent, since for a list of size `n`, passing in `n` as the index returns a different value than passing in `-1`, when they should be equivalent. This can lead to us failing to hit the early return in `ltrim()` when the operation is a no-op and sending a delta that we don't need to.
   
   This method should return the exclusive index to be used in `clearSublist()` so that we can be consistent with how Java deals with indexes and not have to add 1 to the value before passing it into other methods, i.e. it should return `size -1` if the index passed to it is greater than or equal to `size`, or if the index is `-1`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";

Review comment:
       This is only used in once place in the test and never referenced elsewhere, so it can be inlined.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsOK() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  @Parameters(method = "getValidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenValidRange(long start, long end, String[] expected) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  // @Parameterized.Parameters(name = "start:{0}, end:{1}, expected:{2}")
+  @SuppressWarnings("unused")
+  private Object[] getValidRanges() {
+    // Values are start, end, expected result
+    // For initial list of {e4, e3, e2, e1}
+    return new Object[] {
+        new Object[] {0L, 0L, new String[] {"e4"}},
+        new Object[] {0L, 1L, new String[] {"e4", "e3"}},
+        new Object[] {0L, 2L, new String[] {"e4", "e3", "e2"}},
+        new Object[] {1L, 2L, new String[] {"e3", "e2"}},
+        new Object[] {1L, -1L, new String[] {"e3", "e2", "e1"}},
+        new Object[] {1L, -2L, new String[] {"e3", "e2"}},
+        new Object[] {-2L, -1L, new String[] {"e2", "e1"}},
+        new Object[] {-1L, -1L, new String[] {"e1"}},
+        new Object[] {0L, 3L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {2L, 3L, new String[] {"e2", "e1"}},
+        new Object[] {3L, 4L, new String[] {"e1"}},
+        new Object[] {0L, 4L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {0L, 10L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-5L, -1L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-10L, 10L, new String[] {"e4", "e3", "e2", "e1"}}
+    };
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);

Review comment:
       This line is not necessary, as we call `flushAll()` in the `@Before` method, which gets called before each iteration of the test, which means that this line can be removed and the method can be inlined.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsOK() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  @Parameters(method = "getValidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenValidRange(long start, long end, String[] expected) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  // @Parameterized.Parameters(name = "start:{0}, end:{1}, expected:{2}")
+  @SuppressWarnings("unused")
+  private Object[] getValidRanges() {
+    // Values are start, end, expected result
+    // For initial list of {e4, e3, e2, e1}
+    return new Object[] {
+        new Object[] {0L, 0L, new String[] {"e4"}},
+        new Object[] {0L, 1L, new String[] {"e4", "e3"}},
+        new Object[] {0L, 2L, new String[] {"e4", "e3", "e2"}},
+        new Object[] {1L, 2L, new String[] {"e3", "e2"}},
+        new Object[] {1L, -1L, new String[] {"e3", "e2", "e1"}},
+        new Object[] {1L, -2L, new String[] {"e3", "e2"}},
+        new Object[] {-2L, -1L, new String[] {"e2", "e1"}},
+        new Object[] {-1L, -1L, new String[] {"e1"}},
+        new Object[] {0L, 3L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {2L, 3L, new String[] {"e2", "e1"}},
+        new Object[] {3L, 4L, new String[] {"e1"}},
+        new Object[] {0L, 4L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {0L, 10L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-5L, -1L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-10L, 10L, new String[] {"e4", "e3", "e2", "e1"}}
+    };
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenAllElementsTrimmed() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, -5);

Review comment:
       This test could be expanded to "removesKey_whenRangeIsEmpty()" and parameterized to use various ranges that do not contain any elements e.g. both start and end are past the end of the list, both start and end are before the start of the list etc.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -221,6 +222,63 @@ public int llen() {
     return elementList.size();
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,

Review comment:
       I think the method return type being `Void` here would still be a nice improvement, since it's misleading to imply that a `byte[]` is returned when this method always returns `null`. Also, `start` and `end` are now passed in as `int`, so the method signature (and the signatures of `getBoundedStartIndex()` and `getBoundedEndIndex()`) should be changed to reflect that.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {

Review comment:
       Since this is a custom implementation rather than just an overridden one that calls the `super()` method, we should have comprehensive unit tests for this method. It's not something that should be addressed as part of this PR, but we're also missing unit tests for several other public methods on this class, which should definitely be added. It would be good to file a JIRA ticket to track that testing gap.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
##########
@@ -58,6 +58,24 @@ public void getSizeInBytesIsAccurate_ForSizeableByteArrayListElements() {
     assertThat(list.size()).isEqualTo(0);
   }
 
+  @Test
+  public void clearSublist_getSizeInBytesIsAccurate() {
+    // Create a list with an initial size and confirm that it correctly reports its size
+    SizeableByteArrayList list = createList();
+    assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+
+    // Remove subset of elements and assert that the size is correct after each remove
+    Random rand = new Random();
+    while (list.size() > 3) {
+      int fromIndex = rand.nextInt(list.size() / 2);
+      int toIndex = rand.nextInt(list.size() / 2) + fromIndex;

Review comment:
       This test doesn't need to use randomness; that just complicates things and could potentially lead to a more difficult to diagnose bug if the test does fail. For this test, it would be enough to just create a list, remove some sublist from the middle once, then assert that the size is correct.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() / 2) {
+      clearFromBeginning(fromIndex, toIndex);
+    } else {
+      clearFromEnd(fromIndex, toIndex);
+    }
+  }
+
+  public void clearFromBeginning(int fromIndex, int toIndex) {

Review comment:
       This method should be private.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest implements RedisIntegrationTest {

Review comment:
       To help catch potential edge cases, could we have a parameterized test for the behaviour when the list contains only one element? I added some trace logging and played around a bit and noticed that there was some inconsistent/unexpected behaviour when the list contains only one element that it would be good to explicitly test.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() / 2) {
+      clearFromBeginning(fromIndex, toIndex);
+    } else {
+      clearFromEnd(fromIndex, toIndex);
+    }
+  }
+
+  public void clearFromBeginning(int fromIndex, int toIndex) {
+    ListIterator<byte[]> iterator = listIterator(fromIndex);
+    int removeCount = toIndex - fromIndex;
+    int count = 0;
+
+    while (iterator.hasNext() && count < removeCount) {
+      byte[] element = iterator.next();
+      iterator.remove();
+      count++;
+      memberOverhead -= calculateByteArrayOverhead(element);
+    }
+  }
+
+  private void clearFromEnd(int fromIndex, int toIndex) {
+    ListIterator<byte[]> descendingIterator = listIterator(toIndex);
+    int removedCount = toIndex - fromIndex;
+
+    while (descendingIterator.hasPrevious() && removedCount > 0) {
+      byte[] element = descendingIterator.previous();
+      descendingIterator.remove();
+      removedCount--;
+      memberOverhead -= calculateByteArrayOverhead(element);
+    }
+  }

Review comment:
       For consistency, could this method be modified to match the implementation of `clearFromBeginning()`:
   ```
     private void clearFromEnd(int fromIndex, int toIndex) {
       ListIterator<byte[]> descendingIterator = listIterator(toIndex);
       int removeCount = toIndex - fromIndex;
       int count = 0;
   
       while (descendingIterator.hasPrevious() && count < removeCount) {
         byte[] element = descendingIterator.previous();
         descendingIterator.remove();
         count++;
         memberOverhead -= calculateByteArrayOverhead(element);
       }
   ```

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
##########
@@ -0,0 +1,181 @@
+/*
+ * 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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean isRunning = new AtomicBoolean(true);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), isRunning, elementList1);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), isRunning, elementList2);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(keys.get(2), isRunning, elementList3);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    isRunning.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key,
+      AtomicBoolean isRunning,
+      List<String> elementList) {
+    while (isRunning.get()) {
+      lpushPerformAndVerify(key, elementList);
+
+      for (int i = 1; i <= INITIAL_LIST_SIZE / 2 && isRunning.get(); i++) {
+        long lastIndex = jedis.llen(key) - 2;
+        try {
+          assertThat(jedis.lindex(key, 0))
+              .as("lpush head verification failed at iteration " + i)
+              .isEqualTo(makeElementString(key, INITIAL_LIST_SIZE - i));
+          assertThat(jedis.lindex(key, lastIndex))
+              .as("lpush tail verification failed at iteration " + i)
+              .isEqualTo(makeElementString(key, i));
+          jedis.ltrim(key, 1, lastIndex);
+          assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastIndex);
+        } catch (Throwable ex) {
+          isRunning.set(false); // test is over
+          throw ex;
+        }
+      }
+      if (isRunning.get()) {
+        assertThat(jedis.exists(key)).isFalse();
+      }

Review comment:
       This can be simplified somewhat by using negative indexes and doing the assertions after performing LTRIM (since we already assert on the contents of the list as part of `lpushPerformAndVerify()` so we should never see the list be wrong before we've even done an LTRIM):
   ```
         for (int i = 1; i < INITIAL_LIST_SIZE / 2 && isRunning.get(); i++) {
           try {
             jedis.ltrim(key, 1, -2);
             assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(INITIAL_LIST_SIZE - (i * 2L));
             assertThat(jedis.lindex(key, 0))
                 .as("LTRIM head verification failed at iteration " + i)
                 .isEqualTo(makeElementString(key, INITIAL_LIST_SIZE - 1 - i));
             assertThat(jedis.lindex(key, -1))
                 .as("LTRIM tail verification failed at iteration " + i)
                 .isEqualTo(makeElementString(key, i));
           } catch (Throwable ex) {
             isRunning.set(false); // test is over
             throw ex;
           }
         }
         if (isRunning.get()) {
           jedis.ltrim(key, 1, -2);
           assertThat(jedis.exists(key)).isFalse();
         }
   ```

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() / 2) {

Review comment:
       This attempt at optimization is not working the way you intend, I think. For a list of 100 elements with `fromIndex = 49`, `toIndex = 100`, you end up starting at the beginning of the list and iterating all 100 elements, rather than starting at the end and only having to iterate 51. The correct optimization would be to check which of `fromIndex` and `toIndex` is closer to its respective end of the list, and then clear from that side.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }

Review comment:
       It might be nice to add a test to `RedisListTest.java` to confirm that we hit this early return under various conditions, as I noticed that for lists with just one element, we were still sending a delta in some situations even though the list wasn't modified at all.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsOK() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  @Parameters(method = "getValidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenValidRange(long start, long end, String[] expected) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  // @Parameterized.Parameters(name = "start:{0}, end:{1}, expected:{2}")

Review comment:
       This comment should be removed.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
##########
@@ -0,0 +1,181 @@
+/*
+ * 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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean isRunning = new AtomicBoolean(true);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), isRunning, elementList1);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), isRunning, elementList2);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(keys.get(2), isRunning, elementList3);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100; i++) {

Review comment:
       in order to have the test stop early if `ltrimPerformAndVerify()` encounters an exception, you need to also include a check for `isRunning.get()` in the for loop condition.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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


   This pull request **fixes 1 alert** when merging ac9b48bd7717cc81c1a13f556c5f8a61c047e4c6 into dfedcbd409ae4f724b4d7ca02f576a8e401b7d6c - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-084813c364dc3496df33ade8c37fc7a410be2058)
   
   **fixed 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 #7403: GEODE-9953: Implement LTRIM Command

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


   This pull request **fixes 1 alert** when merging 9202fef3572204600d36ad85893b70bd2bdfb67c into 6806179b67bac90d18aa23adcb68413942433636 - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-d92f087b954cf9b23e6ab7a5f817639485459a56)
   
   **fixed 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] jdeppe-pivotal commented on a change in pull request #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.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.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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 10000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicLong runningCount = new AtomicLong(3);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    lpushPerformAndVerify(keys.get(0), elementList1);
+    lpushPerformAndVerify(keys.get(1), elementList2);
+    lpushPerformAndVerify(keys.get(2), elementList3);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), runningCount);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), runningCount);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(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 < 100 && runningCount.get() > 0; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    runningCount.set(0);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key, AtomicLong runningCount) {
+    assertThat(jedis.llen(key)).isEqualTo(INITIAL_LIST_SIZE);
+
+    int elementCount = INITIAL_LIST_SIZE - 1;
+    int i = 0;
+    while (jedis.llen(key) > 1 && runningCount.get() > 0) {
+      String expected = makeElementString(key, i);
+      try {
+        assertThat(jedis.lindex(key, elementCount)).isEqualTo(expected);
+        jedis.ltrim(key, 0, elementCount - 1);
+        assertThat(jedis.llen(key)).isEqualTo(elementCount);
+        elementCount--;
+        i++;
+      } catch (Exception ex) {

Review comment:
       If you want a failing assertion to stop this loop (and thus the test) you'll have to catch a `Throwable` here. Prefer rather to add an `.as` clause to the assertion with the details you're adding to the RuntimeException and just rethrow the caught exception.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -114,7 +115,8 @@ public void givenBucketsMovedDuringLPush_elementsAreAddedAtomically()
 
     for (String key : keys) {
       long length = jedis.llen(key);
-      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      assertThat(length).isCloseTo(ITERATION_COUNT * 2 * PUSH_LIST_SIZE, within(6L));
+      assertThat(length % 3).isEqualTo(0);

Review comment:
       I think this assertion can be exact and shouldn't need the offset.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -82,28 +92,93 @@ public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region
    */
   public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
     byte[] popped = elementRemove(0);
-    RemoveElementsByIndex removed = new RemoveElementsByIndex();
+    RemoveElementsByIndex removed = new RemoveElementsByIndex(version);
     removed.add(0);
     storeChanges(region, key, removed);
     return popped;
   }
 
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this set; NOTE this list may be 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 int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    elementsPush(elementsToAdd);
+    storeChanges(region, key, new AddByteArraysVersioned(version, elementsToAdd));
     return elementList.size();
   }
 
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int preservedVersion;
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      preservedVersion = version;
+    }
+    System.out.println(
+        "DEBUG ltrim, preserved version:" + preservedVersion + " (version:" + version + ")");
+    storeChanges(region, key, new RemoveElementsByIndex(preservedVersion, removed));
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, -1);
+    }
+  }
+
   @Override
-  public void applyAddByteArrayDelta(byte[] bytes) {
-    elementPush(bytes);
+  public void applyAddByteArrayVersionedDelta(int version, byte[] bytes) {
+    System.out.println("DEBUG abav: local version:" + this.version + " incoming:" + version);
+    if (version != this.version) {
+      elementPush(bytes);
+    }
+    this.version = version;
   }
 
   @Override
-  public void applyRemoveElementsByIndex(List<Integer> indexes) {
-    for (int index : indexes) {
-      elementRemove(index);
+  public void applyRemoveElementsByIndex(int version, List<Integer> indexes) {
+    System.out.println("DEBUG arebi: local version:" + this.version + " incoming:" + version);
+    synchronized (this) {
+      if (version != this.version) {
+        elementsRemove(indexes);
+      }
+      this.version = version;

Review comment:
       There shouldn't be any need to synchronize here since this is a delta application and there will be locking at the Geode level to protect the entry. ~~I think this should just be:~~
   
   < Deleted 'cos I'm an idiot>
   
   ~~You want to update the version to match the change being applied. Similarly in `applyAddByteArrayVersionedDelta`.~~




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -359,8 +359,10 @@ public int scard() {
   }
 
   @Override
-  public void applyAddByteArrayDelta(byte[] bytes) {
-    membersAdd(bytes);
+  public void applyAddByteArrayDelta(List<byte[]> byteArrays) {

Review comment:
       Mooted after rebase

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,

Review comment:
       Fixed

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
##########
@@ -0,0 +1,57 @@
+/*
+ * 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_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      byte[] startI = commandElems.get(startIndex);
+      byte[] stopI = commandElems.get(stopIndex);
+      start = Coder.bytesToLong(startI);
+      end = Coder.bytesToLong(stopI);
+    } catch (NumberFormatException e) {
+      return RedisResponse.error(ERROR_NOT_INTEGER);
+    }
+
+    byte[] retVal =
+        context.listLockedExecute(key, false, list -> list.ltrim(start, end, region, key));
+    // return RedisResponse.error(ERROR_NOT_INTEGER);

Review comment:
       Yanked

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
##########
@@ -0,0 +1,57 @@
+/*
+ * 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_NOT_INTEGER;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LTrimExecutor implements CommandExecutor {
+  private static final int startIndex = 2;
+  private static final int stopIndex = 3;
+
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+
+    long start;
+    long end;
+
+    try {
+      byte[] startI = commandElems.get(startIndex);
+      byte[] stopI = commandElems.get(stopIndex);
+      start = Coder.bytesToLong(startI);
+      end = Coder.bytesToLong(stopI);

Review comment:
       Streamlined by inlining

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,228 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved_multipleTimes() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();
+  }
+
+  @Test
+  public void withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "troix"};

Review comment:
       Mais oui!

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,228 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -10, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 4);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 10);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenLastElementRemoved() {
+    final String keyWithTagForKeysCommand = "{tag}" + KEY;
+    jedis.lpush(keyWithTagForKeysCommand, "e1", "e2", "e3");
+
+    jedis.ltrim(keyWithTagForKeysCommand, 0, -4);
+    assertThat(jedis.llen(keyWithTagForKeysCommand)).isEqualTo(0L);
+    assertThat(jedis.keys(keyWithTagForKeysCommand)).isEmpty();

Review comment:
       Done for both

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,228 @@
+/*
+ * 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_NOT_INTEGER;
+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 AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsNull() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void trimsToSpecifiedRange_givenValidRange() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 0);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, 2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(3);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, 1, -2);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e2");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -2, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(2);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e1");
+
+    initializeTestList();
+
+    jedis.ltrim(KEY, -1, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(1);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e1");
+  }
+
+  @Test
+  public void trimsToCorrectRange_givenSpecifiersOutsideListSize() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, -4, -1);
+    assertThat(jedis.llen(KEY)).isEqualTo(4);
+    assertThat(jedis.lindex(KEY, 0)).isEqualTo("e4");
+    assertThat(jedis.lindex(KEY, 1)).isEqualTo("e3");
+    assertThat(jedis.lindex(KEY, 2)).isEqualTo("e2");
+    assertThat(jedis.lindex(KEY, 3)).isEqualTo("e1");
+
+    initializeTestList();

Review comment:
       In this test case, you're right.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -422,16 +424,21 @@ synchronized boolean membersRemove(byte[] memberToRemove) {
    * @return the number of members actually added
    */
   public long sadd(List<byte[]> membersToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
-    AddByteArrays delta = new AddByteArrays();
+    AddByteArrays delta;
+    byte newVersion;
     int membersAdded = 0;
-    for (byte[] member : membersToAdd) {
-      if (membersAdd(member)) {
-        delta.add(member);
-        membersAdded++;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      delta = new AddByteArrays(newVersion);
+      for (byte[] member : membersToAdd) {
+        if (membersAdd(member)) {
+          delta.add(member);
+          membersAdded++;
+        }
+      }
+      if (membersAdded == 0) {

Review comment:
       No longer touching RedisSet after rebase

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -422,16 +424,21 @@ synchronized boolean membersRemove(byte[] memberToRemove) {
    * @return the number of members actually added
    */
   public long sadd(List<byte[]> membersToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
-    AddByteArrays delta = new AddByteArrays();
+    AddByteArrays delta;
+    byte newVersion;
     int membersAdded = 0;
-    for (byte[] member : membersToAdd) {
-      if (membersAdd(member)) {
-        delta.add(member);
-        membersAdded++;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();

Review comment:
       Reworked




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LPushDUnitTest.java
##########
@@ -114,7 +115,8 @@ public void givenBucketsMovedDuringLPush_elementsAreAddedAtomically()
 
     for (String key : keys) {
       long length = jedis.llen(key);
-      assertThat(length).isGreaterThanOrEqualTo(MINIMUM_ITERATIONS * 2 * PUSH_LIST_SIZE);
+      assertThat(length).isCloseTo(ITERATION_COUNT * 2 * PUSH_LIST_SIZE, within(6L));
+      assertThat(length % 3).isEqualTo(0);

Review comment:
       I have seen failures, but that was an earlier version. Ran a bunch of these without failures, so I'm making it exact.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {

Review comment:
       GEODE-10204 created.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
##########
@@ -58,6 +58,24 @@ public void getSizeInBytesIsAccurate_ForSizeableByteArrayListElements() {
     assertThat(list.size()).isEqualTo(0);
   }
 
+  @Test
+  public void clearSublist_getSizeInBytesIsAccurate() {
+    // Create a list with an initial size and confirm that it correctly reports its size
+    SizeableByteArrayList list = createList();
+    assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+
+    // Remove subset of elements and assert that the size is correct after each remove
+    Random rand = new Random();
+    while (list.size() > 3) {
+      int fromIndex = rand.nextInt(list.size() / 2);
+      int toIndex = rand.nextInt(list.size() / 2) + fromIndex;

Review comment:
       Deterministic now.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest implements RedisIntegrationTest {

Review comment:
       Can you think of more cases to add to the new trimsToSpecifiedRange_givenListWithOneElement() test?




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,56 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   */
+  public Void ltrim(int start, int end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      elementsRetainByIndexRange(boundedStart, boundedEnd);
+
+      retainElementsByRange =
+          new RetainElementsByIndexRange(incrementAndGetVersion(), boundedStart, boundedEnd);
+    }
+    storeChanges(region, key, retainElementsByRange);
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {

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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();
+    RemoveElementsByIndex removeElementsByIndex;
+
+    synchronized (this) {
+      if (boundedStart > boundedEnd || boundedStart == length) {
+        // Remove everything
+        for (int i = length - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      } else {
+        // Remove any elements after boundedEnd
+        for (int i = length - 1; i > boundedEnd; i--) {
+          removed.add(i);
+        }
+
+        // Remove any elements before boundedStart
+        for (int i = boundedStart - 1; i >= 0; i--) {
+          removed.add(i);
+        }
+      }
+
+      if (removed.size() > 0) {
+        elementsRemove(removed);
+      }
+      newVersion = incrementAndGetVersion();

Review comment:
       Lots of this reworked, partly due to the rebase.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -127,50 +127,139 @@ private int getArrayIndex(int listIndex) {
   }
 
   /**
-   * @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
-   * @param onlyIfExists if true then the elements should only be added if the key already exists
-   *        and holds a list, otherwise no operation is performed.
-   * @return the length of the list after the operation
-   */
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
-      final boolean onlyIfExists) {
-    elementsPush(elementsToAdd);
-    storeChanges(region, key, new AddByteArrays(elementsToAdd));
+   * @return the number of elements in the list
+   **/
+  public int llen() {
     return elementList.size();
   }
 
   /**
    * @param region the region this instance is stored in
-   * @param key the name of the set to add to
+   * @param key the name of the list 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);
+    byte newVersion;
+    byte[] popped;
+    RemoveElementsByIndex removed;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      popped = removeFirstElement();
+      removed = new RemoveElementsByIndex(newVersion);
+      removed.add(0);
+    }
     storeChanges(region, key, removed);
     return popped;
   }
 
+  public synchronized byte[] removeFirstElement() {
+    return elementList.removeFirst();
+  }
+
+  public synchronized byte[] removeLastElement() {
+    return elementList.removeLast();
+  }
+
   /**
-   * @return the number of elements in the list
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @param onlyIfExists if true then the elements should only be added if the key already exists
+   *        and holds a list, otherwise no operation is performed.
+   * @return the length of the list after the operation
    */
-  public int llen() {
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key,
+      final boolean onlyIfExists) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
     return elementList.size();
   }
 
+  /**
+   * @param elementsToAdd elements to add to this list; NOTE this list may be modified by this call
+   * @param region the region this instance is stored in
+   * @param key the name of the list to add to
+   * @return the number of elements actually added
+   */
+  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> region, RedisKey key) {
+    byte newVersion;
+    AddByteArrays addByteArrays;
+    synchronized (this) {
+      newVersion = incrementAndGetVersion();
+      elementsPush(elementsToAdd);
+      addByteArrays = new AddByteArrays(newVersion, elementsToAdd);
+    }
+    storeChanges(region, key, addByteArrays);
+    return elementList.size();
+  }
+
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    byte newVersion;
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+    List<Integer> removed = new ArrayList<>();

Review comment:
       Got a different scheme now, just sending the start and end indexes over the wire (one of them negative if the whole list is gone).




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
##########
@@ -0,0 +1,181 @@
+/*
+ * 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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean isRunning = new AtomicBoolean(true);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), isRunning, elementList1);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), isRunning, elementList2);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(keys.get(2), isRunning, elementList3);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100; i++) {

Review comment:
       Conditionalized.

##########
File path: geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
##########
@@ -0,0 +1,181 @@
+/*
+ * 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.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+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 LTrimDUnitTest {
+  public static final int INITIAL_LIST_SIZE = 5_000;
+
+  @Rule
+  public RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule();
+
+  @Rule
+  public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private static JedisCluster jedis;
+
+  @Before
+  public void testSetup() {
+    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);
+    clusterStartUp.flushAll();
+  }
+
+  @After
+  public void tearDown() {
+    jedis.close();
+  }
+
+  @Test
+  public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash() {
+    String key = makeListKeyWithHashtag(1, clusterStartUp.getKeyOnServer("ltrim", 1));
+    List<String> elementList = makeElementList(key, INITIAL_LIST_SIZE);
+    lpushPerformAndVerify(key, elementList);
+
+    // Remove all but last element
+    jedis.ltrim(key, INITIAL_LIST_SIZE - 1, INITIAL_LIST_SIZE);
+
+    clusterStartUp.crashVM(1); // kill primary server
+
+    assertThat(jedis.lindex(key, 0)).isEqualTo(elementList.get(0));
+    jedis.ltrim(key, 0, -2);
+    assertThat(jedis.exists(key)).isFalse();
+  }
+
+  @Test
+  public void givenBucketsMoveDuringLtrim_thenOperationsAreNotLost() throws Exception {
+    AtomicBoolean isRunning = new AtomicBoolean(true);
+    List<String> listHashtags = makeListHashtags();
+    List<String> keys = makeListKeys(listHashtags);
+
+    List<String> elementList1 = makeElementList(keys.get(0), INITIAL_LIST_SIZE);
+    List<String> elementList2 = makeElementList(keys.get(1), INITIAL_LIST_SIZE);
+    List<String> elementList3 = makeElementList(keys.get(2), INITIAL_LIST_SIZE);
+
+    Runnable task1 =
+        () -> ltrimPerformAndVerify(keys.get(0), isRunning, elementList1);
+    Runnable task2 =
+        () -> ltrimPerformAndVerify(keys.get(1), isRunning, elementList2);
+    Runnable task3 =
+        () -> ltrimPerformAndVerify(keys.get(2), isRunning, elementList3);
+
+    Future<Void> future1 = executor.runAsync(task1);
+    Future<Void> future2 = executor.runAsync(task2);
+    Future<Void> future3 = executor.runAsync(task3);
+
+    for (int i = 0; i < 100; i++) {
+      clusterStartUp.moveBucketForKey(listHashtags.get(i % listHashtags.size()));
+      Thread.sleep(200);
+    }
+
+    isRunning.set(false);
+
+    future1.get();
+    future2.get();
+    future3.get();
+  }
+
+  private List<String> makeListHashtags() {
+    List<String> listHashtags = new ArrayList<>();
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 1));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 2));
+    listHashtags.add(clusterStartUp.getKeyOnServer("ltrim", 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).as("Initial list lengths not equal for key %s'", key)
+        .isEqualTo(elementList.size());
+  }
+
+  private void ltrimPerformAndVerify(String key,
+      AtomicBoolean isRunning,
+      List<String> elementList) {
+    while (isRunning.get()) {
+      lpushPerformAndVerify(key, elementList);
+
+      for (int i = 1; i <= INITIAL_LIST_SIZE / 2 && isRunning.get(); i++) {
+        long lastIndex = jedis.llen(key) - 2;
+        try {
+          assertThat(jedis.lindex(key, 0))
+              .as("lpush head verification failed at iteration " + i)
+              .isEqualTo(makeElementString(key, INITIAL_LIST_SIZE - i));
+          assertThat(jedis.lindex(key, lastIndex))
+              .as("lpush tail verification failed at iteration " + i)
+              .isEqualTo(makeElementString(key, i));
+          jedis.ltrim(key, 1, lastIndex);
+          assertThat(jedis.llen(key)).as("Key: %s ", key).isEqualTo(lastIndex);
+        } catch (Throwable ex) {
+          isRunning.set(false); // test is over
+          throw ex;
+        }
+      }
+      if (isRunning.get()) {
+        assertThat(jedis.exists(key)).isFalse();
+      }

Review comment:
       Reworked.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsOK() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  @Parameters(method = "getValidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenValidRange(long start, long end, String[] expected) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  // @Parameterized.Parameters(name = "start:{0}, end:{1}, expected:{2}")
+  @SuppressWarnings("unused")
+  private Object[] getValidRanges() {
+    // Values are start, end, expected result
+    // For initial list of {e4, e3, e2, e1}
+    return new Object[] {
+        new Object[] {0L, 0L, new String[] {"e4"}},
+        new Object[] {0L, 1L, new String[] {"e4", "e3"}},
+        new Object[] {0L, 2L, new String[] {"e4", "e3", "e2"}},
+        new Object[] {1L, 2L, new String[] {"e3", "e2"}},
+        new Object[] {1L, -1L, new String[] {"e3", "e2", "e1"}},
+        new Object[] {1L, -2L, new String[] {"e3", "e2"}},
+        new Object[] {-2L, -1L, new String[] {"e2", "e1"}},
+        new Object[] {-1L, -1L, new String[] {"e1"}},
+        new Object[] {0L, 3L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {2L, 3L, new String[] {"e2", "e1"}},
+        new Object[] {3L, 4L, new String[] {"e1"}},
+        new Object[] {0L, 4L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {0L, 10L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-5L, -1L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-10L, 10L, new String[] {"e4", "e3", "e2", "e1"}}
+    };
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  public void removesKey_whenAllElementsTrimmed() {
+    initializeTestList();
+
+    jedis.ltrim(KEY, 0, -5);

Review comment:
       Parameterized.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsOK() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  @Parameters(method = "getValidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenValidRange(long start, long end, String[] expected) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  // @Parameterized.Parameters(name = "start:{0}, end:{1}, expected:{2}")

Review comment:
       Good catch, yanked.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest implements RedisIntegrationTest {
+  public static final String KEY = "key";
+  public static final String PREEXISTING_VALUE = "preexistingValue";

Review comment:
       Inlined.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest 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 givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LTRIM, 3);
+  }
+
+  @Test
+  public void withNonListKey_Fails() {
+    jedis.set("string", PREEXISTING_VALUE);
+    assertThatThrownBy(() -> jedis.ltrim("string", 0, -1))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void withNonExistentKey_returnsOK() {
+    assertThat(jedis.ltrim("nonexistent", 0, -1)).isEqualTo("OK");
+  }
+
+  @Test
+  public void withNonIntegerRangeSpecifier_Fails() {
+    jedis.lpush(KEY, "e1", "e2", "e3", "e4");
+
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "0", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "-1"))
+            .hasMessage(ERROR_NOT_INTEGER);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.LTRIM, KEY,
+        "not-an-integer", "not-an-integer"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  @Parameters(method = "getValidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenValidRange(long start, long end, String[] expected) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  // @Parameterized.Parameters(name = "start:{0}, end:{1}, expected:{2}")
+  @SuppressWarnings("unused")
+  private Object[] getValidRanges() {
+    // Values are start, end, expected result
+    // For initial list of {e4, e3, e2, e1}
+    return new Object[] {
+        new Object[] {0L, 0L, new String[] {"e4"}},
+        new Object[] {0L, 1L, new String[] {"e4", "e3"}},
+        new Object[] {0L, 2L, new String[] {"e4", "e3", "e2"}},
+        new Object[] {1L, 2L, new String[] {"e3", "e2"}},
+        new Object[] {1L, -1L, new String[] {"e3", "e2", "e1"}},
+        new Object[] {1L, -2L, new String[] {"e3", "e2"}},
+        new Object[] {-2L, -1L, new String[] {"e2", "e1"}},
+        new Object[] {-1L, -1L, new String[] {"e1"}},
+        new Object[] {0L, 3L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {2L, 3L, new String[] {"e2", "e1"}},
+        new Object[] {3L, 4L, new String[] {"e1"}},
+        new Object[] {0L, 4L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {0L, 10L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-5L, -1L, new String[] {"e4", "e3", "e2", "e1"}},
+        new Object[] {-10L, 10L, new String[] {"e4", "e3", "e2", "e1"}}
+    };
+  }
+
+  private void initializeTestList() {
+    jedis.del(KEY);

Review comment:
       Simplified.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() / 2) {
+      clearFromBeginning(fromIndex, toIndex);
+    } else {
+      clearFromEnd(fromIndex, toIndex);
+    }
+  }
+
+  public void clearFromBeginning(int fromIndex, int toIndex) {

Review comment:
       Privatized.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() / 2) {
+      clearFromBeginning(fromIndex, toIndex);
+    } else {
+      clearFromEnd(fromIndex, toIndex);
+    }
+  }
+
+  public void clearFromBeginning(int fromIndex, int toIndex) {
+    ListIterator<byte[]> iterator = listIterator(fromIndex);
+    int removeCount = toIndex - fromIndex;
+    int count = 0;
+
+    while (iterator.hasNext() && count < removeCount) {
+      byte[] element = iterator.next();
+      iterator.remove();
+      count++;
+      memberOverhead -= calculateByteArrayOverhead(element);
+    }
+  }
+
+  private void clearFromEnd(int fromIndex, int toIndex) {
+    ListIterator<byte[]> descendingIterator = listIterator(toIndex);
+    int removedCount = toIndex - fromIndex;
+
+    while (descendingIterator.hasPrevious() && removedCount > 0) {
+      byte[] element = descendingIterator.previous();
+      descendingIterator.remove();
+      removedCount--;
+      memberOverhead -= calculateByteArrayOverhead(element);
+    }
+  }

Review comment:
       Updated.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
##########
@@ -83,6 +83,39 @@
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() / 2) {

Review comment:
       Optimized.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      if (boundedEnd < length) {
+        // trim stuff at end of list
+        elementList.clearSublist(boundedEnd + 1, length);
+      }
+      if (boundedStart > 0) {
+        // trim stuff at start of list
+        elementList.clearSublist(0, boundedStart);
+      }

Review comment:
       De-duped.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;
+    }
+
+    if (boundedStart == 0 && boundedEnd == length) {
+      // No-op, return without modifying the list
+      return null;
+    }
+
+    RetainElementsByIndexRange retainElementsByRange;
+    synchronized (this) {
+      if (boundedEnd < length) {
+        // trim stuff at end of list
+        elementList.clearSublist(boundedEnd + 1, length);
+      }
+      if (boundedStart > 0) {
+        // trim stuff at start of list
+        elementList.clearSublist(0, boundedStart);
+      }
+      retainElementsByRange =
+          new RetainElementsByIndexRange(incrementAndGetVersion(), boundedStart, boundedEnd);
+    }
+    storeChanges(region, key, retainElementsByRange);
+    return null;
+  }
+
+  private int getBoundedStartIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(long index, int size) {
+    if (index >= 0L) {
+      return (int) Math.min(index, size);
+    } else {
+      return (int) Math.max(index + size, -1);
+    }

Review comment:
       Updated.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -252,6 +253,63 @@ public void lset(Region<RedisKey, RedisData> region, RedisKey key, int index, by
     storeChanges(region, key, new ReplaceByteArrayAtOffset(index, value));
   }
 
+  /**
+   * @param start the index of the first element to retain
+   * @param end the index of the last element to retain
+   * @param region the region this instance is stored in
+   * @param key the name of the list to pop from
+   * @return the element actually popped
+   */
+  public byte[] ltrim(long start, long end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    int length = elementList.size();
+    int boundedStart = getBoundedStartIndex(start, length);
+    int boundedEnd = getBoundedEndIndex(end, length);
+
+    if (boundedStart > boundedEnd || boundedStart == length) {
+      // Remove everything
+      region.remove(key);
+      return null;

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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -287,15 +287,15 @@ public Void ltrim(int start, int end, Region<RedisKey, RedisData> region,
     return null;
   }
 
-  private int getBoundedStartIndex(long index, int size) {
+  private int getBoundedStartIndex(int index, int size) {
     if (index >= 0L) {
       return (int) Math.min(index, size);
     } else {
       return (int) Math.max(index + size, 0);

Review comment:
       These casts are now redundant and can be removed, as are the ones in `getBoundedEndIndex()`.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -173,4 +195,10 @@ private RedisList createRedisList(int e1, int e2) {
     newList.elementPushHead(new byte[] {(byte) e2});
     return newList;
   }
+
+  private RedisList createRedisListWithOneElement(int e1) {
+    RedisList newList = new RedisList();
+    newList.elementPushHead(new byte[] {(byte) e1});

Review comment:
       For simplicity, could this method just take a `byte[]` as the argument rather than an int that we then have to fiddle with to put into the list?

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -143,6 +143,28 @@ public void versionDoesNotUpdateWhenReferenceElementNotFound() {
     assertThat(list.getVersion()).isEqualTo(originalVersion);
   }
 
+  @Test
+  public void versionDoesNotUpdateWhenLtrimDoesNotModifyList() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(PartitionedRegion.class));
+    RedisList list = createRedisListWithDuplicateElements();
+
+    byte originalVersion = list.getVersion();
+    list.ltrim(0, 5, region, null);

Review comment:
       To make it more obvious what this is doing, could the end index be `-1`? That way, regardless of the size of the list, we're guaranteed to always do a no-op.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -147,6 +147,31 @@ public void removesKey_whenRangeIsEmpty(long start, long end) {
     };
   }
 
+  @Test
+  @Parameters(method = "getRangesForOneElementList")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenListWithOneElement(long start, long end,
+      String[] expected) {
+    jedis.lpush(KEY, "e1");
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  @SuppressWarnings("unused")
+  private Object[] getRangesForOneElementList() {
+    // Values are start, end, expected
+    // For initial list of {e4, e3, e2, e1}

Review comment:
       This comment is incorrect

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -0,0 +1,160 @@
+/*
+ * 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_NOT_INTEGER;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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;
+import org.apache.geode.test.junit.runners.GeodeParamsRunner;
+
+@RunWith(GeodeParamsRunner.class)
+public abstract class AbstractLTrimIntegrationTest implements RedisIntegrationTest {

Review comment:
       What you have looks comprehensive to me




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
##########
@@ -147,6 +147,31 @@ public void removesKey_whenRangeIsEmpty(long start, long end) {
     };
   }
 
+  @Test
+  @Parameters(method = "getRangesForOneElementList")
+  @TestCaseName("{method}: start:{0}, end:{1}, expected:{2}")
+  public void trimsToSpecifiedRange_givenListWithOneElement(long start, long end,
+      String[] expected) {
+    jedis.lpush(KEY, "e1");
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.lrange(KEY, 0, -1)).containsExactly(expected);
+  }
+
+  @SuppressWarnings("unused")
+  private Object[] getRangesForOneElementList() {
+    // Values are start, end, expected
+    // For initial list of {e4, e3, e2, e1}

Review comment:
       Yanked, since it's pretty simple for a one-element list.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
##########
@@ -287,15 +287,15 @@ public Void ltrim(int start, int end, Region<RedisKey, RedisData> region,
     return null;
   }
 
-  private int getBoundedStartIndex(long index, int size) {
+  private int getBoundedStartIndex(int index, int size) {
     if (index >= 0L) {
       return (int) Math.min(index, size);
     } else {
       return (int) Math.max(index + size, 0);

Review comment:
       Swept away.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -143,6 +143,28 @@ public void versionDoesNotUpdateWhenReferenceElementNotFound() {
     assertThat(list.getVersion()).isEqualTo(originalVersion);
   }
 
+  @Test
+  public void versionDoesNotUpdateWhenLtrimDoesNotModifyList() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(PartitionedRegion.class));
+    RedisList list = createRedisListWithDuplicateElements();
+
+    byte originalVersion = list.getVersion();
+    list.ltrim(0, 5, region, null);

Review comment:
       Done.

##########
File path: geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
##########
@@ -173,4 +195,10 @@ private RedisList createRedisList(int e1, int e2) {
     newList.elementPushHead(new byte[] {(byte) e2});
     return newList;
   }
+
+  private RedisList createRedisListWithOneElement(int e1) {
+    RedisList newList = new RedisList();
+    newList.elementPushHead(new byte[] {(byte) e1});

Review comment:
       Since we don't care about the contents of the element, now there's no arg at all.




-- 
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 #7403: GEODE-9953: Implement LTRIM Command

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


   This pull request **fixes 1 alert** when merging f64db770a2bfba23576c2133772e505e42000bf9 into 4a73cb726d11a454dd8d77151ee1862efa0d22bd - [view on LGTM.com](https://lgtm.com/projects/g/apache/geode/rev/pr-9ff25f0a79ac05da21903f4f8456c78d44ca294a)
   
   **fixed 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