You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by ri...@apache.org on 2022/04/04 19:35:36 UTC

[geode] branch develop updated: GEODE-9953: Implement LTRIM Command (#7403)

This is an automated email from the ASF dual-hosted git repository.

ringles pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 3cc3bdb826 GEODE-9953: Implement LTRIM Command (#7403)
3cc3bdb826 is described below

commit 3cc3bdb826ee5047b3fec1b4d9de92fb1fdabc83
Author: Ray Ingles <ri...@pivotal.io>
AuthorDate: Mon Apr 4 15:35:30 2022 -0400

    GEODE-9953: Implement LTRIM Command (#7403)
    
    * GEODE-9953: Implement  Redis LTRIM command
    
    * Docs updated
    
    Co-authored-by: Ray Ingles <ri...@vmware.com>
---
 .../tools_modules/geode_for_redis.html.md.erb      |  36 ++--
 geode-for-redis/README.md                          |   4 +
 .../list/LTrimNativeRedisAcceptanceTest.java}      |  39 ++---
 .../commands/executor/list/LTrimDUnitTest.java     | 181 +++++++++++++++++++
 .../executor/list/AbstractLPopIntegrationTest.java |   4 +-
 .../list/AbstractLTrimIntegrationTest.java         | 194 +++++++++++++++++++++
 .../executor/list/AbstractRPopIntegrationTest.java |   2 +-
 .../executor/list/LTrimIntegrationTest.java}       |  37 +---
 .../redis/internal/commands/RedisCommandType.java  |   2 +
 .../commands/executor/list/LTrimExecutor.java      |  54 ++++++
 .../redis/internal/data/AbstractRedisData.java     |   9 +
 .../geode/redis/internal/data/NullRedisList.java   |   6 +
 .../geode/redis/internal/data/RedisList.java       |  77 +++++++-
 .../apache/geode/redis/internal/data/RedisSet.java |   6 +-
 .../data/collections/SizeableByteArrayList.java    |  36 +++-
 .../geode/redis/internal/data/delta/DeltaType.java |   3 +-
 .../data/delta/RetainElementsByIndexRange.java     |  52 ++++++
 .../geode/redis/internal/data/RedisListTest.java   |  28 +++
 .../collections/SizeableByteArrayListTest.java     |  14 ++
 19 files changed, 701 insertions(+), 83 deletions(-)

diff --git a/geode-docs/tools_modules/geode_for_redis.html.md.erb b/geode-docs/tools_modules/geode_for_redis.html.md.erb
index de593c61b9..23f3b8912d 100644
--- a/geode-docs/tools_modules/geode_for_redis.html.md.erb
+++ b/geode-docs/tools_modules/geode_for_redis.html.md.erb
@@ -189,24 +189,24 @@ Could not connect to Redis at 127.0.0.1:6379: Connection refused
 | INCRBY             | INCRBYFLOAT        | INFO **[4]**       | KEYS               |
 | LINDEX             | LINSERT            | LLEN               | LOLWUT             |
 | LPOP               | LPUSH              | LPUSHX             | LRANGE             |
-| LREM               | LSET               | MGET               | MSET               |
-| MSETNX             | PERSIST            | PEXPIRE            | PEXPIREAT          |
-| PING               | PSETEX             | PSUBSCRIBE         | PTTL               |
-| PUBLISH            | PUBSUB             | PUNSUBSCRIBE       | RENAME             |
-| RENAMENX           | RESTORE            | RPOP               | RPOPLPUSH          |
-| RPUSH              | RPUSHX             | SADD               | SCARD              |
-| SDIFF              | SDIFFSTORE         | SET                | SETEX              |
-| SETNX              | SETRANGE           | SINTER             | SINTERSTORE        |
-| SISMEMBER          | SMEMBERS           | SMOVE              | SPOP               |
-| SRANDMEMBER        | SREM               | SSCAN **[3]**      | STRLEN             |
-| SUBSCRIBE          | SUNION             | SUNIONSTORE        | TTL                |
-| TYPE               | UNSUBSCRIBE        | QUIT               | ZADD               |
-| ZCARD              | ZCOUNT             | ZINCRBY            | ZINTERSTORE        |
-| ZLEXCOUNT          | ZPOPMAX            | ZPOPMIN            | ZRANGE             |
-| ZRANGEBYLEX        | ZRANGEBYSCORE      | ZRANK              | ZREM               |
-| ZREMRANGEBYLEX     | ZREMRANGEBYRANK    | ZREMRANGEBYSCORE   | ZREVRANGE          |
-| ZREVRANGEBYLEX     | ZREVRANGEBYSCORE   | ZREVRANK           | ZSCAN **[3]**      |
-| ZSCORE             | ZUNIONSTORE        |                    |                    |
+| LREM               | LSET               | LTRIM              | MGET               |
+| MSET               | MSETNX             | PERSIST            | PEXPIRE            |
+| PEXPIREAT          | PING               | PSETEX             | PSUBSCRIBE         |
+| PTTL               | PUBLISH            | PUBSUB             | PUNSUBSCRIBE       |
+| RENAME             | RENAMENX           | RESTORE            | RPOP               |
+| RPOPLPUSH          | RPUSH              | RPUSHX             | SADD               |
+| SCARD              | SDIFF              | SDIFFSTORE         | SET                |
+| SETEX              | SETNX              | SETRANGE           | SINTER             |
+| SINTERSTORE        | SISMEMBER          | SMEMBERS           | SMOVE              |
+| SPOP               | SRANDMEMBER        | SREM               | SSCAN **[3]**      |
+| STRLEN             | SUBSCRIBE          | SUNION             | SUNIONSTORE        |
+| TTL                | TYPE               | UNSUBSCRIBE        | QUIT               |
+| ZADD               | ZCARD              | ZCOUNT             | ZINCRBY            |
+| ZINTERSTORE        | ZLEXCOUNT          | ZPOPMAX            | ZPOPMIN            |
+| ZRANGE             | ZRANGEBYLEX        | ZRANGEBYSCORE      | ZRANK              |
+| ZREM               | ZREMRANGEBYLEX     | ZREMRANGEBYRANK    | ZREMRANGEBYSCORE   |
+| ZREVRANGE          | ZREVRANGEBYLEX     | ZREVRANGEBYSCORE   | ZREVRANK           |
+| ZSCAN **[3]**      | ZSCORE             | ZUNIONSTORE        |                    |
 
 Commands not listed above are **not implemented**.
 
diff --git a/geode-for-redis/README.md b/geode-for-redis/README.md
index 9cdc9a43a1..5a56bec04c 100644
--- a/geode-for-redis/README.md
+++ b/geode-for-redis/README.md
@@ -183,7 +183,11 @@ Geode for Redis implements a subset of the full Redis command set.
 - INFO <sup>2</sup>  
 - KEYS  
 - LINDEX
+- LLEN
+- LPUSH
+- LPOP
 - LRANGE
+- LTRIM
 - LREM
 - MGET
 - MSET
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java b/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimNativeRedisAcceptanceTest.java
old mode 100644
new mode 100755
similarity index 54%
copy from geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
copy to geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimNativeRedisAcceptanceTest.java
index 7d1d5eba5a..97e5421dfe
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
+++ b/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimNativeRedisAcceptanceTest.java
@@ -11,39 +11,26 @@
  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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;
 
-package org.apache.geode.redis.internal.data.delta;
+import org.junit.ClassRule;
 
-public enum DeltaType {
-  ADD_BYTE_ARRAYS(true),
-  ADD_BYTE_ARRAYS_TAIL(true),
-  ADD_BYTE_ARRAY_PAIRS,
-  ADD_BYTE_ARRAY_DOUBLE_PAIRS,
-  APPEND_BYTE_ARRAY(true),
-  INSERT_BYTE_ARRAY(true),
-  REMOVE_BYTE_ARRAYS,
-  REPLACE_BYTE_ARRAYS,
-  REPLACE_BYTE_ARRAY_AT_OFFSET,
-  REPLACE_BYTE_ARRAY_DOUBLE_PAIRS,
-  REPLACE_BYTE_AT_OFFSET,
-  SET_BYTE_ARRAY,
-  SET_BYTE_ARRAY_AND_TIMESTAMP,
-  SET_TIMESTAMP,
-  REMOVE_ELEMENTS_BY_INDEX(true);
+import org.apache.geode.redis.NativeRedisClusterTestRule;
 
-  private final boolean versioned;
+public class LTrimNativeRedisAcceptanceTest extends AbstractLTrimIntegrationTest {
 
-  DeltaType() {
-    this(false);
-  }
+  @ClassRule
+  public static NativeRedisClusterTestRule redis = new NativeRedisClusterTestRule();
 
-  DeltaType(boolean versioned) {
-    this.versioned = versioned;
+  @Override
+  public int getPort() {
+    return redis.getExposedPorts().get(0);
   }
 
-  public boolean isVersioned() {
-    return versioned;
+  @Override
+  public void flushAll() {
+    redis.flushAll();
   }
+
 }
diff --git a/geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java b/geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimDUnitTest.java
new file mode 100644
index 0000000000..35ba68f777
--- /dev/null
+++ b/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 && isRunning.get(); 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++) {
+        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();
+      }
+    }
+  }
+
+  private String makeListKeyWithHashtag(int index, String hashtag) {
+    return "{" + hashtag + "}-key-" + index;
+  }
+
+  private String makeElementString(String key, int iterationCount) {
+    return "-" + key + "-" + iterationCount + "-";
+  }
+
+  private List<String> makeElementList(String key, int listSize) {
+    List<String> elementList = new ArrayList<>();
+    for (int i = 0; i < listSize; i++) {
+      elementList.add(makeElementString(key, i));
+    }
+    return elementList;
+  }
+}
diff --git a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
index a0e24a0c91..e4fa7659cd 100755
--- a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
+++ b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLPopIntegrationTest.java
@@ -94,7 +94,7 @@ public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTes
 
   @Test
   public void lpop_withConcurrentLPush_returnsCorrectValue() {
-    String[] valuesInitial = new String[] {"un", "deux", "troix"};
+    String[] valuesInitial = new String[] {"un", "deux", "trois"};
     String[] valuesToAdd = new String[] {"plum", "peach", "orange"};
     jedis.lpush(KEY, valuesInitial);
 
@@ -105,7 +105,7 @@ public abstract class AbstractLPopIntegrationTest implements RedisIntegrationTes
             .runWithAction(() -> {
               assertThat(lpopReference).satisfiesAnyOf(
                   lpopResult -> assertThat(lpopReference.get()).isEqualTo("orange"),
-                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("troix"));
+                  lpopResult -> assertThat(lpopReference.get()).isEqualTo("trois"));
               jedis.del(KEY);
               jedis.lpush(KEY, valuesInitial);
             });
diff --git a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
new file mode 100755
index 0000000000..fc64febbab
--- /dev/null
+++ b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLTrimIntegrationTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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";
+  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", "preexistingValue");
+    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);
+  }
+
+  @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.lpush(KEY, "e1", "e2", "e3", "e4");
+  }
+
+  @Test
+  @Parameters(method = "getInvalidRanges")
+  @TestCaseName("{method}: start:{0}, end:{1}")
+  public void removesKey_whenRangeIsEmpty(long start, long end) {
+    initializeTestList();
+
+    jedis.ltrim(KEY, start, end);
+    assertThat(jedis.exists(KEY)).isFalse();
+  }
+
+  @SuppressWarnings("unused")
+  private Object[] getInvalidRanges() {
+    // Values are start, end
+    // For initial list of {e4, e3, e2, e1}
+    return new Object[] {
+        new Object[] {0L, -5L},
+        new Object[] {5L, 10L},
+        new Object[] {-5L, -10L}
+    };
+  }
+
+  @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
+    return new Object[] {
+        new Object[] {0L, 0L, new String[] {"e1"}},
+        new Object[] {0L, 1L, new String[] {"e1"}},
+        new Object[] {0L, -1L, new String[] {"e1"}},
+        new Object[] {-1L, 0L, new String[] {"e1"}},
+        new Object[] {-1L, -1L, new String[] {"e1"}},
+        new Object[] {1L, 1L, new String[] {}}
+    };
+  }
+
+  @Test
+  public void withConcurrentLPush_returnsCorrectValue() {
+    String[] valuesInitial = new String[] {"un", "deux", "trois"};
+    String[] valuesToAdd = new String[] {"plum", "peach", "orange"};
+    jedis.lpush(KEY, valuesInitial);
+    List<String> valuesInitialReversed = new ArrayList<>(Arrays.asList("trois", "deux", "un"));
+    List<String> valuesToAddReversed = new ArrayList<>(Arrays.asList("orange", "peach", "plum"));
+    final AtomicReference<String> lpopReference = new AtomicReference<>();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(KEY, valuesToAdd),
+        i -> jedis.ltrim(KEY, 0, 2))
+            .runWithAction(() -> {
+              List<String> result = jedis.lrange(KEY, 0, 2);
+              assertThat(result).satisfiesAnyOf(
+                  lrangeResult -> assertThat(result).isEqualTo(valuesInitialReversed),
+                  lrangeResult -> assertThat(result).isEqualTo(valuesToAddReversed));
+              jedis.del(KEY);
+              jedis.lpush(KEY, valuesInitial);
+            });
+  }
+}
diff --git a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractRPopIntegrationTest.java b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractRPopIntegrationTest.java
index 6f87a70bc8..6687fc0814 100644
--- a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractRPopIntegrationTest.java
+++ b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractRPopIntegrationTest.java
@@ -101,7 +101,7 @@ public abstract class AbstractRPopIntegrationTest implements RedisIntegrationTes
 
   @Test
   public void rpop_withConcurrentLPush_returnsCorrectValue() {
-    String[] valuesInitial = new String[] {"un", "deux", "troix"};
+    String[] valuesInitial = new String[] {"un", "deux", "trois"};
     String[] valuesToAdd = new String[] {"plum", "peach", "orange"};
     jedis.lpush(KEY, valuesInitial);
 
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimIntegrationTest.java
old mode 100644
new mode 100755
similarity index 54%
copy from geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
copy to geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimIntegrationTest.java
index 7d1d5eba5a..740500ac40
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
+++ b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LTrimIntegrationTest.java
@@ -11,39 +11,20 @@
  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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;
 
-package org.apache.geode.redis.internal.data.delta;
-
-public enum DeltaType {
-  ADD_BYTE_ARRAYS(true),
-  ADD_BYTE_ARRAYS_TAIL(true),
-  ADD_BYTE_ARRAY_PAIRS,
-  ADD_BYTE_ARRAY_DOUBLE_PAIRS,
-  APPEND_BYTE_ARRAY(true),
-  INSERT_BYTE_ARRAY(true),
-  REMOVE_BYTE_ARRAYS,
-  REPLACE_BYTE_ARRAYS,
-  REPLACE_BYTE_ARRAY_AT_OFFSET,
-  REPLACE_BYTE_ARRAY_DOUBLE_PAIRS,
-  REPLACE_BYTE_AT_OFFSET,
-  SET_BYTE_ARRAY,
-  SET_BYTE_ARRAY_AND_TIMESTAMP,
-  SET_TIMESTAMP,
-  REMOVE_ELEMENTS_BY_INDEX(true);
+import org.junit.ClassRule;
 
-  private final boolean versioned;
+import org.apache.geode.redis.GeodeRedisServerRule;
 
-  DeltaType() {
-    this(false);
-  }
+public class LTrimIntegrationTest extends AbstractLTrimIntegrationTest {
 
-  DeltaType(boolean versioned) {
-    this.versioned = versioned;
-  }
+  @ClassRule
+  public static GeodeRedisServerRule server = new GeodeRedisServerRule();
 
-  public boolean isVersioned() {
-    return versioned;
+  @Override
+  public int getPort() {
+    return server.getPort();
   }
 }
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
index 1cb6f0d88c..f3d3aaf3a4 100755
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
@@ -88,6 +88,7 @@ import org.apache.geode.redis.internal.commands.executor.list.LPushXExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LRangeExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LRemExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LSetExecutor;
+import org.apache.geode.redis.internal.commands.executor.list.LTrimExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.RPopExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.RPopLPushExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.RPushExecutor;
@@ -408,6 +409,7 @@ public enum RedisCommandType {
   LREM(new LRemExecutor(), Category.LIST, SUPPORTED, new Parameter().exact(4).flags(WRITE)),
   LSET(new LSetExecutor(), Category.LIST, SUPPORTED,
       new Parameter().exact(4).flags(WRITE, DENYOOM)),
+  LTRIM(new LTrimExecutor(), Category.LIST, SUPPORTED, new Parameter().exact(4).flags(WRITE)),
   RPUSH(new RPushExecutor(), Category.LIST, SUPPORTED,
       new Parameter().min(3).flags(WRITE, DENYOOM, FAST)),
   RPUSHX(new RPushXExecutor(), Category.LIST, SUPPORTED,
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
new file mode 100644
index 0000000000..c521f90edc
--- /dev/null
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LTrimExecutor.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 static org.apache.geode.redis.internal.netty.Coder.bytesToLong;
+import static org.apache.geode.redis.internal.netty.Coder.narrowLongToInt;
+
+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.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();
+
+    int start;
+    int end;
+
+    try {
+      start = narrowLongToInt(bytesToLong(commandElems.get(startIndex)));
+      end = narrowLongToInt(bytesToLong(commandElems.get(stopIndex)));
+    } catch (NumberFormatException e) {
+      return RedisResponse.error(ERROR_NOT_INTEGER);
+    }
+
+    Region<RedisKey, RedisData> region = context.getRegion();
+    RedisKey key = command.getKey();
+    context.listLockedExecute(key, false, list -> list.ltrim(start, end, region, key));
+    return RedisResponse.ok();
+  }
+}
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
index eb0b8f08ff..96f2497d62 100644
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
@@ -31,6 +31,7 @@ import static org.apache.geode.redis.internal.data.delta.DeltaType.REPLACE_BYTE_
 import static org.apache.geode.redis.internal.data.delta.DeltaType.REPLACE_BYTE_ARRAY_AT_OFFSET;
 import static org.apache.geode.redis.internal.data.delta.DeltaType.REPLACE_BYTE_ARRAY_DOUBLE_PAIRS;
 import static org.apache.geode.redis.internal.data.delta.DeltaType.REPLACE_BYTE_AT_OFFSET;
+import static org.apache.geode.redis.internal.data.delta.DeltaType.RETAIN_ELEMENTS_BY_INDEX_RANGE;
 import static org.apache.geode.redis.internal.data.delta.DeltaType.SET_BYTE_ARRAY;
 import static org.apache.geode.redis.internal.data.delta.DeltaType.SET_BYTE_ARRAY_AND_TIMESTAMP;
 import static org.apache.geode.redis.internal.netty.StringBytesGlossary.RADISH_DUMP_HEADER;
@@ -67,6 +68,7 @@ import org.apache.geode.redis.internal.data.delta.ReplaceByteArrayAtOffset;
 import org.apache.geode.redis.internal.data.delta.ReplaceByteArrayDoublePairs;
 import org.apache.geode.redis.internal.data.delta.ReplaceByteArrays;
 import org.apache.geode.redis.internal.data.delta.ReplaceByteAtOffset;
+import org.apache.geode.redis.internal.data.delta.RetainElementsByIndexRange;
 import org.apache.geode.redis.internal.data.delta.SetByteArray;
 import org.apache.geode.redis.internal.data.delta.SetByteArrayAndTimestamp;
 import org.apache.geode.redis.internal.data.delta.SetTimestamp;
@@ -309,6 +311,9 @@ public abstract class AbstractRedisData implements RedisData {
       case REMOVE_ELEMENTS_BY_INDEX:
         RemoveElementsByIndex.deserializeFrom(in, this);
         break;
+      case RETAIN_ELEMENTS_BY_INDEX_RANGE:
+        RetainElementsByIndexRange.deserializeFrom(in, this);
+        break;
     }
   }
 
@@ -373,6 +378,10 @@ public abstract class AbstractRedisData implements RedisData {
     throw new IllegalStateException("unexpected " + REMOVE_ELEMENTS_BY_INDEX);
   }
 
+  public void applyRetainElementsByIndexRange(int start, int end) {
+    throw new IllegalStateException("unexpected " + RETAIN_ELEMENTS_BY_INDEX_RANGE);
+  }
+
   @Override
   public byte[] dump() throws IOException {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
index fc4e990543..a23adc415e 100644
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
@@ -29,6 +29,12 @@ class NullRedisList extends RedisList {
     super();
   }
 
+  @Override
+  public Void ltrim(int start, int end, Region<RedisKey, RedisData> region,
+      RedisKey key) {
+    return null;
+  }
+
   @Override
   public List<byte[]> lrange(int start, int stop) {
     return Collections.emptyList();
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
index 36965192df..1c48f384ef 100644
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
@@ -43,6 +43,7 @@ import org.apache.geode.redis.internal.data.delta.AddByteArraysTail;
 import org.apache.geode.redis.internal.data.delta.InsertByteArray;
 import org.apache.geode.redis.internal.data.delta.RemoveElementsByIndex;
 import org.apache.geode.redis.internal.data.delta.ReplaceByteArrayAtOffset;
+import org.apache.geode.redis.internal.data.delta.RetainElementsByIndexRange;
 import org.apache.geode.redis.internal.eventing.BlockingCommandListener;
 import org.apache.geode.redis.internal.eventing.NotificationEvent;
 import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
@@ -159,7 +160,7 @@ public class RedisList extends AbstractRedisData {
   /**
    * @param context the context of the executing command
    * @param elementsToAdd elements to add to this list
-   * @param key the name of the set to add to
+   * @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
@@ -225,7 +226,7 @@ public class RedisList extends AbstractRedisData {
    *        Negative count starts from the tail and moves to the head.
    * @param element element to remove
    * @param region the region this instance is stored in
-   * @param key the name of the set to add
+   * @param key the name of the list to add
    * @return amount of elements that were actually removed
    */
   public int lrem(int count, byte[] element, Region<RedisKey, RedisData> region, RedisKey key) {
@@ -262,6 +263,56 @@ public class RedisList extends AbstractRedisData {
     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 - 1) {
+      // 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(int index, int size) {
+    if (index >= 0L) {
+      return Math.min(index, size);
+    } else {
+      return Math.max(index + size, 0);
+    }
+  }
+
+  private int getBoundedEndIndex(int index, int size) {
+    if (index >= 0L) {
+      return Math.min(index, size - 1);
+    } else {
+      return Math.max(index + size, -1);
+    }
+  }
+
   /**
    * @param context the context of the executing command
    * @param elementsToAdd elements to add to this list;
@@ -374,12 +425,16 @@ public class RedisList extends AbstractRedisData {
     elementReplace(offset, newValue);
   }
 
+  @Override
+  public void applyRetainElementsByIndexRange(int start, int end) {
+    elementsRetainByIndexRange(start, end);
+  }
+
   /**
    * Since GII (getInitialImage) can come in and call toData while other threads are modifying this
    * object, the striped executor will not protect toData. So any methods that modify "elements"
    * needs to be thread safe with toData.
    */
-
   @Override
   public synchronized void toData(DataOutput out) throws IOException {
     super.toData(out);
@@ -439,10 +494,26 @@ public class RedisList extends AbstractRedisData {
     }
   }
 
+  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 (end < elementList.size()) {
+      elementList.clearSublist(end + 1, elementList.size());
+    }
+
+    if (start > 0) {
+      elementList.clearSublist(0, start);
+    }
+  }
+
   protected synchronized void elementPushTail(byte[] element) {
     elementList.addLast(element);
   }
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
index 4bc1a44ba0..17a8b8a66f 100644
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
@@ -437,9 +437,9 @@ public class RedisSet extends AbstractRedisData {
           membersAdded++;
         }
       }
-      if (membersAdded == 0) {
-        return 0;
-      }
+    }
+    if (membersAdded == 0) {
+      return 0;
     }
     storeChanges(region, key, delta);
     return membersAdded;
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
index 78e4b798c4..fedc73470b 100644
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
@@ -33,7 +33,7 @@ public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeabl
   private int memberOverhead;
 
   /**
-   * @param toRemove element to remove from the list
+   * @param elementToRemove element to remove from the list
    * @param count number of elements that match object o to remove from the list.
    *        Count that is equal to 0 removes all matching elements from the list.
    * @return list of indexes that were removed in order.
@@ -83,6 +83,40 @@ public class SizeableByteArrayList extends LinkedList<byte[]> implements Sizeabl
     return indexesRemoved;
   }
 
+  public void clearSublist(int fromIndex, int toIndex) {
+    if (fromIndex < size() - toIndex) {
+      clearFromBeginning(fromIndex, toIndex);
+    } else {
+      clearFromEnd(fromIndex, toIndex);
+    }
+  }
+
+  private 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 removeCount = toIndex - fromIndex;
+    int count = 0;
+
+    while (descendingIterator.hasPrevious() && count < removeCount) {
+      byte[] element = descendingIterator.previous();
+      descendingIterator.remove();
+      count++;
+      memberOverhead -= calculateByteArrayOverhead(element);
+    }
+  }
+
   @Override
   public int indexOf(Object o) {
     ListIterator<byte[]> iterator = this.listIterator();
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
index 7d1d5eba5a..c02fb40505 100644
--- a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
+++ b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/DeltaType.java
@@ -31,7 +31,8 @@ public enum DeltaType {
   SET_BYTE_ARRAY,
   SET_BYTE_ARRAY_AND_TIMESTAMP,
   SET_TIMESTAMP,
-  REMOVE_ELEMENTS_BY_INDEX(true);
+  REMOVE_ELEMENTS_BY_INDEX(true),
+  RETAIN_ELEMENTS_BY_INDEX_RANGE(true);
 
   private final boolean versioned;
 
diff --git a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RetainElementsByIndexRange.java b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RetainElementsByIndexRange.java
new file mode 100644
index 0000000000..61e917f02b
--- /dev/null
+++ b/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 final int start;
+  private final int end;
+
+  public RetainElementsByIndexRange(byte version, int start, int end) {
+    super(version);
+    this.start = start;
+    this.end = end;
+  }
+
+  @Override
+  public DeltaType getType() {
+    return RETAIN_ELEMENTS_BY_INDEX_RANGE;
+  }
+
+  public void serializeTo(DataOutput out) throws IOException {
+    super.serializeTo(out);
+    DataSerializer.writePrimitiveInt(start, out);
+    DataSerializer.writePrimitiveInt(end, out);
+  }
+
+  public static void deserializeFrom(DataInput in, AbstractRedisData redisData) throws IOException {
+    redisData.applyRetainElementsByIndexRange(readPrimitiveInt(in), readPrimitiveInt(in));
+  }
+}
diff --git a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
index 031a1809d7..3d0bf0d605 100644
--- a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
+++ b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
@@ -141,6 +141,28 @@ public class RedisListTest {
     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, -1, region, null);
+
+    assertThat(list.getVersion()).isEqualTo(originalVersion);
+  }
+
+  @Test
+  public void versionDoesNotUpdateWhenLtrimDoesNotModifyOneElementList() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(PartitionedRegion.class));
+    RedisList list = createRedisListWithOneElement();
+
+    byte originalVersion = list.getVersion();
+    list.ltrim(0, 1, region, null);
+
+    assertThat(list.getVersion()).isEqualTo(originalVersion);
+  }
+
   private Object validateDeltaSerialization(InvocationOnMock invocation) throws IOException {
     RedisList value = invocation.getArgument(1, RedisList.class);
     assertThat(value.hasDelta()).isTrue();
@@ -171,4 +193,10 @@ public class RedisListTest {
     newList.elementPushHead(new byte[] {(byte) e2});
     return newList;
   }
+
+  private RedisList createRedisListWithOneElement() {
+    RedisList newList = new RedisList();
+    newList.elementPushHead("e1".getBytes());
+    return newList;
+  }
 }
diff --git a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
index 2cf1ebb9bf..5fb6015724 100644
--- a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
+++ b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
@@ -58,6 +58,20 @@ public class SizeableByteArrayListTest {
     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
+    list.clearSublist(INITIAL_NUMBER_OF_ELEMENTS / 5, INITIAL_NUMBER_OF_ELEMENTS / 2);
+    assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+
+    list.clearSublist(0, list.size());
+    assertThat(list.size()).isEqualTo(0);
+  }
+
   @Test
   public void removeObjects_getSizeInBytesIsAccurate() {
     // Create a list with an initial size and confirm that it correctly reports its size