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