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

jensdeppe 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 2cd5d82  GEODE-9950: Add Radish LRANGE command (#7389)
2cd5d82 is described below

commit 2cd5d8271357b76e76c584577ae0c214cc5cdc16
Author: Kris10 <[email protected]>
AuthorDate: Mon Mar 7 08:39:34 2022 -0800

    GEODE-9950: Add Radish LRANGE command (#7389)
    
    This implements a version of the Redis LRANGE command, which is used for 
list data types. Associated tests were also added.
    
    Given a key, it returns a list of elements in the range (inclusive) for the 
specified start index and stop index. Both the start and stop indexes are zero 
based, which starts at the head of the list. Negative indexes start at the tail 
of the list.
    
    LINDEX was modified to use a new method created in this commit. The method 
converts negative indexes to the corresponding positive index.
---
 .../tools_modules/geode_for_redis.html.md.erb      |   8 +-
 geode-for-redis/README.md                          |   1 +
 .../list/LRangeNativeRedisAcceptanceTest.java}     |  42 ++---
 .../list/AbstractLRangeIntegrationTest.java        | 186 +++++++++++++++++++++
 .../executor/list/LRangeIntegrationTest.java}      |  44 +----
 .../server/AbstractHitsMissesIntegrationTest.java  |   9 +-
 .../redis/internal/commands/RedisCommandType.java  |   2 +
 .../commands/executor/list/LRangeExecutor.java     |  48 ++++++
 .../geode/redis/internal/data/NullRedisList.java   |   6 +
 .../geode/redis/internal/data/RedisList.java       |  76 ++++++++-
 10 files changed, 343 insertions(+), 79 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 dce2066..f75daf2 100644
--- a/geode-docs/tools_modules/geode_for_redis.html.md.erb
+++ b/geode-docs/tools_modules/geode_for_redis.html.md.erb
@@ -188,10 +188,10 @@ Could not connect to Redis at 127.0.0.1:6379: Connection 
refused
 | HSETNX | HSTRLEN | HVALS | INCR |
 | INCRBY | INCRBYFLOAT | INFO **[4]** | KEYS |
 | LINDEX | LLEN | LOLWUT | LPOP |
-| LPUSH | LPUSHX | MGET | MSET |
-| MSETNX | PERSIST | PEXPIRE | PEXPIREAT |
-| PING | PSETEX | PSUBSCRIBE | PTTL |
-| PUBLISH PUBSUB | PUNSUBSCRIBE | RENAME |
+| LPUSH | LPUSHX | LRANGE | MGET |
+| MSET | MSETNX | PERSIST | PEXPIRE |
+| PEXPIREAT | PING | PSETEX | PSUBSCRIBE |
+| PTTL | PUBLISH PUBSUB | PUNSUBSCRIBE | RENAME |
 | RENAMENX | RESTORE | SADD | SCARD |
 | SDIFF | SDIFFSTORE | SET | SETEX |
 | SETNX | SETRANGE | SINTER | SINTERSTORE |
diff --git a/geode-for-redis/README.md b/geode-for-redis/README.md
index 2c954e9..7cd1531 100644
--- a/geode-for-redis/README.md
+++ b/geode-for-redis/README.md
@@ -183,6 +183,7 @@ Geode for Redis implements a subset of the full Redis 
command set.
 - INFO <sup>2</sup>  
 - KEYS  
 - LINDEX
+- LRANGE
 - MGET
 - MSET
 - MSETNX
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
 
b/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LRangeNativeRedisAcceptanceTest.java
old mode 100644
new mode 100755
similarity index 52%
copy from 
geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
copy to 
geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LRangeNativeRedisAcceptanceTest.java
index 65006eb..6e4b27f
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
+++ 
b/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LRangeNativeRedisAcceptanceTest.java
@@ -11,48 +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;
-
-import java.util.List;
+import org.junit.ClassRule;
 
-import org.apache.geode.cache.Region;
+import org.apache.geode.redis.NativeRedisClusterTestRule;
 
-class NullRedisList extends RedisList {
+public class LRangeNativeRedisAcceptanceTest extends 
AbstractLRangeIntegrationTest {
 
-  NullRedisList() {
-    super();
-  }
+  @ClassRule
+  public static NativeRedisClusterTestRule redis = new 
NativeRedisClusterTestRule();
 
   @Override
-  public boolean isNull() {
-    return true;
+  public int getPort() {
+    return redis.getExposedPorts().get(0);
   }
 
   @Override
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> 
region, RedisKey key,
-      final boolean onlyIfExists) {
-    if (onlyIfExists) {
-      return 0;
-    }
-
-    RedisList newList = new RedisList();
-    for (byte[] element : elementsToAdd) {
-      newList.elementPush(element);
-    }
-    region.create(key, newList);
-    return elementsToAdd.size();
+  public void flushAll() {
+    redis.flushAll();
   }
 
-  @Override
-  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
-    return null;
-  }
-
-  @Override
-  public int llen() {
-    return 0;
-  }
 }
diff --git 
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLRangeIntegrationTest.java
 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLRangeIntegrationTest.java
new file mode 100755
index 0000000..2b82d86
--- /dev/null
+++ 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLRangeIntegrationTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, 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.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLRangeIntegrationTest implements 
RedisIntegrationTest {
+  private static final String NON_EXISTENT_LIST_KEY = "{tag1}nonExistentKey";
+  private static final String LIST_KEY = "{tag1}listKey";
+  private static final String[] LIST_ELEMENTS =
+      {"aardvark", "bats", "chameleon", "deer", "elephant", "flamingo", 
"goat"};
+  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 lrange_wrongNumberOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LRANGE, 3);
+  }
+
+  @Test
+  public void 
lrange_withNonExistentList_withStartIndexLessThanStopIndex_returnsEmptyList() {
+    assertThat(jedis.lrange(NON_EXISTENT_LIST_KEY, -10, 10)).isEmpty();
+  }
+
+  @Test
+  public void 
lrange_withNonExistentList_withStartIndexGreaterThanStopIndex_returnsEmptyList()
 {
+    assertThat(jedis.lrange(NON_EXISTENT_LIST_KEY, 10, -10)).isEmpty();
+  }
+
+  @Test
+  public void lrange_withInvalidRange_returnsEmptyList() {
+    jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+    assertThat(getLRangeResult(4, 0)).isEmpty();
+    assertThat(getLRangeResult(2, -10)).isEmpty();
+    assertThat(getLRangeResult(-3, 0)).isEmpty();
+    assertThat(getLRangeResult(-4, -6)).isEmpty();
+    assertThat(getLRangeResult(-5, -10)).isEmpty();
+    assertThat(getLRangeResult(10, 5)).isEmpty();
+    assertThat(getLRangeResult(10, -6)).isEmpty();
+    assertThat(getLRangeResult(8, 10)).isEmpty();
+    assertThat(getLRangeResult(10, -10)).isEmpty();
+    assertThat(getLRangeResult(-12, -10)).isEmpty();
+  }
+
+  @Test
+  public void lrange_withValidRange_returnsElementsInRange() {
+    jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+    String[] result1 = {"goat", "flamingo", "elephant", "deer"};
+    assertThat(getLRangeResult(0, -4)).containsExactly(result1);
+    assertThat(getLRangeResult(-10, 3)).containsExactly(result1);
+
+    String[] result2 = {"chameleon", "bats", "aardvark"};
+    assertThat(getLRangeResult(4, -1)).containsExactly(result2);
+    assertThat(getLRangeResult(4, 10)).containsExactly(result2);
+    assertThat(getLRangeResult(-3, 10)).containsExactly(result2);
+
+    String[] result3 = {"goat", "flamingo", "elephant", "deer", "chameleon", 
"bats"};
+    assertThat(getLRangeResult(-10, -2)).containsExactly(result3);
+
+    String[] result4 = {"goat", "flamingo", "elephant", "deer", "chameleon", 
"bats", "aardvark"};
+    assertThat(getLRangeResult(0, 6)).containsExactly(result4);
+    assertThat(getLRangeResult(-10, 10)).containsExactly(result4);
+  }
+
+  @Test
+  public void lrange_withSameValueForStartIndexAndStopIndex_returnsElement() {
+    jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+    assertThat(getLRangeResult(0, 0)).containsExactly(new String[] {"goat"});
+  }
+
+  @Test
+  public void 
lrange_withInvalidInputs_withNonExisistentList_returnsErrorNotInteger() {
+    assertThatThrownBy(
+        () -> jedis.sendCommand(LIST_KEY, Protocol.Command.LRANGE, 
NON_EXISTENT_LIST_KEY, "b", "2"))
+            .hasMessage(ERROR_NOT_INTEGER);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(LIST_KEY, Protocol.Command.LRANGE, 
NON_EXISTENT_LIST_KEY, "2", "b"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void lrange_withInvalidInputs_returnsErrorNotInteger() {
+    jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(LIST_KEY, Protocol.Command.LRANGE, LIST_KEY, 
"b", "2"))
+            .hasMessage(ERROR_NOT_INTEGER);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(LIST_KEY, Protocol.Command.LRANGE, LIST_KEY, 
"2", "b"))
+            .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void lrange_withWrongTypeKey_returnsErrorWrongType() {
+    String key = "{tag1}ding";
+    jedis.set(key, "dong");
+    assertThatThrownBy(() -> jedis.sendCommand(key, Protocol.Command.LRANGE, 
key, "0", "2"))
+        .hasMessage(ERROR_WRONG_TYPE);
+  }
+
+  @Test
+  public void 
lrange_withWrongTypeKey_withInvalidInputs_returnsErrorNotInteger() {
+    String key = "{tag1}ding";
+    jedis.set(key, "dong");
+    assertThatThrownBy(() -> jedis.sendCommand(key, Protocol.Command.LRANGE, 
key, "b", "2"))
+        .hasMessage(ERROR_NOT_INTEGER);
+
+    assertThatThrownBy(() -> jedis.sendCommand(key, Protocol.Command.LRANGE, 
key, "2", "b"))
+        .hasMessage(ERROR_NOT_INTEGER);
+  }
+
+  @Test
+  public void ensureListConsistency_whenRunningConcurrently() {
+    jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+    final String[] result =
+        {"goat", "flamingo", "elephant", "deer", "chameleon", "bats", 
"aardvark"};
+
+    final String[] elementsToAdd =
+        {"vainglorious", "williwaw", "xiphoid", "ypsiliform", 
"zinziberaceous"};
+    final String[] resultWithElementsAdded =
+        {"zinziberaceous", "ypsiliform", "xiphoid", "williwaw", 
"vainglorious", "goat", "flamingo",
+            "elephant", "deer", "chameleon", "bats", "aardvark"};
+    final AtomicReference<List<String>> lrangeResultReference = new 
AtomicReference<>();
+    new ConcurrentLoopingThreads(1000,
+        i -> jedis.lpush(LIST_KEY, elementsToAdd),
+        i -> lrangeResultReference.set(jedis.lrange(LIST_KEY, -15, 15)))
+            .runWithAction(() -> {
+              assertThat(lrangeResultReference).satisfiesAnyOf(
+                  lrangeResult -> assertThat(lrangeResult.get())
+                      .containsExactly(resultWithElementsAdded),
+                  lrangeResult -> assertThat(lrangeResult.get())
+                      .containsExactly(result));
+              jedis.del(LIST_KEY);
+              jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+            });
+  }
+
+  private List<String> getLRangeResult(int startIndex, int stopIndex) {
+    return jedis.lrange(LIST_KEY, startIndex, stopIndex);
+  }
+}
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LRangeIntegrationTest.java
old mode 100644
new mode 100755
similarity index 50%
copy from 
geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
copy to 
geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LRangeIntegrationTest.java
index 65006eb..f185976
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
+++ 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LRangeIntegrationTest.java
@@ -11,48 +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;
-
-import java.util.List;
+import org.junit.ClassRule;
 
-import org.apache.geode.cache.Region;
+import org.apache.geode.redis.GeodeRedisServerRule;
 
-class NullRedisList extends RedisList {
-
-  NullRedisList() {
-    super();
-  }
-
-  @Override
-  public boolean isNull() {
-    return true;
-  }
+public class LRangeIntegrationTest extends AbstractLRangeIntegrationTest {
 
-  @Override
-  public long lpush(List<byte[]> elementsToAdd, Region<RedisKey, RedisData> 
region, RedisKey key,
-      final boolean onlyIfExists) {
-    if (onlyIfExists) {
-      return 0;
-    }
-
-    RedisList newList = new RedisList();
-    for (byte[] element : elementsToAdd) {
-      newList.elementPush(element);
-    }
-    region.create(key, newList);
-    return elementsToAdd.size();
-  }
-
-  @Override
-  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
-    return null;
-  }
+  @ClassRule
+  public static GeodeRedisServerRule server = new GeodeRedisServerRule();
 
   @Override
-  public int llen() {
-    return 0;
+  public int getPort() {
+    return server.getPort();
   }
 }
diff --git 
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
index a492de4..75a0ccd 100644
--- 
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
+++ 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
@@ -579,6 +579,11 @@ public abstract class AbstractHitsMissesIntegrationTest 
implements RedisIntegrat
   }
 
   @Test
+  public void testLlen() {
+    runCommandAndAssertHitsAndMisses(LIST_KEY, k -> jedis.llen(k));
+  }
+
+  @Test
   public void testLpush() {
     runCommandAndAssertNoStatUpdates(LIST_KEY, k -> jedis.lpush(k, "element"));
   }
@@ -594,8 +599,8 @@ public abstract class AbstractHitsMissesIntegrationTest 
implements RedisIntegrat
   }
 
   @Test
-  public void testLlen() {
-    runCommandAndAssertHitsAndMisses(LIST_KEY, k -> jedis.llen(k));
+  public void testLrange() {
+    runCommandAndAssertHitsAndMisses(LIST_KEY, k -> jedis.lrange(k, 0, 0));
   }
 
   /************* Helper Methods *************/
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 bc9796f..51c1f09 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
@@ -83,6 +83,7 @@ import 
org.apache.geode.redis.internal.commands.executor.list.LLenExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LPopExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LPushExecutor;
 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.pubsub.PsubscribeExecutor;
 import org.apache.geode.redis.internal.commands.executor.pubsub.PubSubExecutor;
 import 
org.apache.geode.redis.internal.commands.executor.pubsub.PublishExecutor;
@@ -391,6 +392,7 @@ public enum RedisCommandType {
       new Parameter().min(3).flags(WRITE, DENYOOM, FAST)),
   LPUSHX(new LPushXExecutor(), Category.LIST, SUPPORTED,
       new Parameter().min(3).flags(WRITE, DENYOOM, FAST)),
+  LRANGE(new LRangeExecutor(), Category.LIST, SUPPORTED, new 
Parameter().exact(4).flags(READONLY)),
 
   /********** Publish Subscribe **********/
 
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LRangeExecutor.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LRangeExecutor.java
new file mode 100644
index 0000000..350ca22
--- /dev/null
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LRangeExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional 
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache 
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the 
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software 
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
KIND, 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.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.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LRangeExecutor implements CommandExecutor {
+  @Override
+  public RedisResponse executeCommand(Command command, ExecutionHandlerContext 
context) {
+    List<byte[]> commandElems = command.getProcessedCommand();
+
+    int startIndex;
+    int stopIndex;
+    try {
+      startIndex = narrowLongToInt(bytesToLong(commandElems.get(2)));
+      stopIndex = narrowLongToInt(bytesToLong(commandElems.get(3)));
+    } catch (NumberFormatException e) {
+      return RedisResponse.error(ERROR_NOT_INTEGER);
+    }
+
+    RedisKey key = command.getKey();
+    List<byte[]> result = context.listLockedExecute(key, true,
+        list -> list.lrange(startIndex, stopIndex));
+    return RedisResponse.array(result, true);
+  }
+}
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 65006eb..3471d6a 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
@@ -16,6 +16,7 @@
 
 package org.apache.geode.redis.internal.data;
 
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.geode.cache.Region;
@@ -27,6 +28,11 @@ class NullRedisList extends RedisList {
   }
 
   @Override
+  public List<byte[]> lrange(int start, int stop) {
+    return Collections.emptyList();
+  }
+
+  @Override
   public boolean isNull() {
     return true;
   }
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 32f286f..1633d75 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
@@ -22,7 +22,11 @@ import static 
org.apache.geode.redis.internal.data.RedisDataType.REDIS_LIST;
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Objects;
 
 import org.apache.geode.DataSerializer;
@@ -38,28 +42,90 @@ public class RedisList extends AbstractRedisData {
   protected static final int REDIS_LIST_OVERHEAD = 
memoryOverhead(RedisList.class);
   private final SizeableByteArrayList elementList;
 
+  private static final int INVALID_INDEX = -1;
+
   public RedisList() {
     this.elementList = new SizeableByteArrayList();
   }
 
   /**
+   * @param start start index of desired elements
+   * @param stop stop index of desired elements
+   * @return list of elements in the range (inclusive).
+   */
+  public List<byte[]> lrange(int start, int stop) {
+    start = normalizeStartIndex(start);
+    stop = normalizeStopIndex(stop);
+
+    int elementSize = elementList.size();
+    if (start > stop || elementSize <= start) {
+      return Collections.emptyList();
+    }
+
+    int resultLength = stop - start + 1;
+
+    // Finds the shortest distance to access nodes in range
+    if (start <= elementSize - stop - 1) {
+      // Starts at head to access nodes at start index then iterates forwards
+      List<byte[]> result = new ArrayList<>(resultLength);
+      ListIterator<byte[]> iterator = elementList.listIterator(start);
+
+      for (int i = start; i <= stop; i++) {
+        byte[] element = iterator.next();
+        result.add(element);
+      }
+      return result;
+
+    } else {
+      // Starts at tail to access nodes at stop index then iterates backwards
+      byte[][] result = new byte[resultLength][];
+      ListIterator<byte[]> iterator = elementList.listIterator(stop + 1);
+
+      for (int i = resultLength - 1; i >= 0; i--) {
+        byte[] element = iterator.previous();
+        result[i] = element;
+      }
+      return Arrays.asList(result);
+    }
+  }
+
+  /**
    * @param index index of desired element. Positive index starts at the head. 
Negative index starts
    *        at the tail.
    * @return element at index. Null if index is out of range.
    */
   public byte[] lindex(int index) {
-    if (index < 0) {
-      // Changes negative index to corresponding positive index to utilize 
get(int index)
-      index = elementList.size() + index;
-    }
+    index = getArrayIndex(index);
 
-    if (index < 0 || elementList.size() <= index) {
+    if (index == INVALID_INDEX || elementList.size() <= index) {
       return null;
     } else {
       return elementList.get(index);
     }
   }
 
+  private int normalizeStartIndex(int startIndex) {
+    return Math.max(0, getArrayIndex(startIndex));
+  }
+
+  private int normalizeStopIndex(int stopIndex) {
+    return Math.min(elementList.size() - 1, getArrayIndex(stopIndex));
+  }
+
+  /**
+   * Changes negative index to corresponding positive index.
+   * If there is no corresponding positive index, returns INVALID_INDEX.
+   */
+  private int getArrayIndex(int listIndex) {
+    if (listIndex < 0) {
+      listIndex = elementList.size() + listIndex;
+      if (listIndex < 0) {
+        return INVALID_INDEX;
+      }
+    }
+    return listIndex;
+  }
+
   /**
    * @param elementsToAdd elements to add to this set; NOTE this list may by 
modified by this call
    * @param region the region this instance is stored in

Reply via email to