diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index bacba0cc6..3f2451437 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -10,6 +10,7 @@ New and noteworthy in the latest releases. * <> support using Lettuce. * <> integration. * `@TypeAlias` Support for Redis repositories. +* Cluster-wide `SCAN` using Lettuce and `SCAN` execution on a selected node supported by both drivers. [[new-in-2.0.0]] == New in Spring Data Redis 2.0 diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java index 0b2e7b5a2..eeac01d59 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java @@ -18,6 +18,8 @@ package org.springframework.data.redis.connection; import java.util.Collection; import java.util.Set; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; /** @@ -49,6 +51,17 @@ public interface RedisClusterConnection extends RedisConnection, RedisClusterCom @Nullable Set keys(RedisClusterNode node, byte[] pattern); + /** + * Use a {@link Cursor} to iterate over keys. + * + * @param node must not be {@literal null}. + * @param options must not be {@literal null}. + * @return never {@literal null}. + * @since 2.1 + * @see Redis Documentation: SCAN + */ + Cursor scan(RedisClusterNode node, ScanOptions options); + /** * @param node must not be {@literal null}. * @return {@literal null} when no keys stored at node or when used in pipeline / transaction. diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java index e49a5015e..770ff6cd6 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -48,6 +48,8 @@ import org.springframework.data.redis.connection.ClusterCommandExecutor.MultiKey import org.springframework.data.redis.connection.ClusterCommandExecutor.NodeResult; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -304,11 +306,24 @@ public class JedisClusterConnection implements DefaultedRedisClusterConnection { return new JedisClusterKeyCommands(this); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#keys(org.springframework.data.redis.connection.RedisClusterNode, byte[]) + */ @Override public Set keys(RedisClusterNode node, byte[] pattern) { return doGetKeyCommands().keys(node, pattern); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#scan(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor scan(RedisClusterNode node, ScanOptions options) { + return doGetKeyCommands().scan(node, options); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisClusterConnection#randomKey(org.springframework.data.redis.connection.RedisClusterNode) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java index affcf7462..070279a46 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.connection.jedis; import lombok.NonNull; import lombok.RequiredArgsConstructor; import redis.clients.jedis.BinaryJedis; +import redis.clients.jedis.ScanParams; import java.util.ArrayList; import java.util.Arrays; @@ -41,6 +42,8 @@ import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisClusterCommandCallback; import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisMultiKeyClusterCommandCallback; import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanCursor; +import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -168,6 +171,28 @@ class JedisClusterKeyCommands implements RedisKeyCommands { throw new InvalidDataAccessApiUsageException("Scan is not supported across multiple nodes within a cluster"); } + Cursor scan(RedisClusterNode node, ScanOptions options) { + + Assert.notNull(node, "RedisClusterNode must not be null!"); + Assert.notNull(options, "Pattern must not be null!"); + + return connection.getClusterCommandExecutor() + .executeCommandOnSingleNode((JedisClusterCommandCallback>) client -> { + + return new ScanCursor(0, options) { + + @Override + protected ScanIteration doScan(long cursorId, ScanOptions options) { + + ScanParams params = JedisConverters.toScanParams(options); + redis.clients.jedis.ScanResult result = client.scan(Long.toString(cursorId), params); + return new ScanIteration<>(Long.valueOf(result.getStringCursor()), + JedisConverters.stringListToByteList().convert(result.getResult())); + } + }.open(); + }, node).getValue(); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisKeyCommands#randomKey() diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java index 900b809b6..c10a33e3f 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnection.java @@ -46,6 +46,8 @@ import org.springframework.data.redis.connection.ClusterCommandExecutor.MultiKey import org.springframework.data.redis.connection.ClusterCommandExecutor.NodeResult; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -491,6 +493,19 @@ public class LettuceClusterConnection extends LettuceConnection implements Defau return doGetClusterKeyCommands().keys(node, pattern); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#scan(org.springframework.data.redis.connection.RedisClusterNode, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor scan(RedisClusterNode node, ScanOptions options) { + return doGetClusterKeyCommands().scan(node, options); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#randomKey(org.springframework.data.redis.connection.RedisClusterNode) + */ public byte[] randomKey(RedisClusterNode node) { return doGetClusterKeyCommands().randomKey(node); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java index 7770f62ad..96da36125 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClusterKeyCommands.java @@ -15,18 +15,21 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.KeyScanCursor; +import io.lettuce.core.ScanArgs; + import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.ClusterSlotHashUtil; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.lettuce.LettuceClusterConnection.LettuceClusterCommandCallback; import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -47,15 +50,6 @@ class LettuceClusterKeyCommands extends LettuceKeyCommands { this.connection = connection; } - /* - * (non-Javadoc) - * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#scan(long, org.springframework.data.redis.core.ScanOptions) - */ - @Override - public Cursor scan(long cursorId, ScanOptions options) { - throw new InvalidDataAccessApiUsageException("Scan is not supported across multiple nodes within a cluster."); - } - /* * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#randomKey() @@ -155,8 +149,8 @@ class LettuceClusterKeyCommands extends LettuceKeyCommands { } /* - * (non-Javadoc) - * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#move(byte[], int) + * (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#move(byte[], int) */ @Override public Boolean move(byte[] key, int dbIndex) { @@ -164,8 +158,8 @@ class LettuceClusterKeyCommands extends LettuceKeyCommands { } /* - * (non-Javadoc) - * @see org.springframework.data.redis.connection.RedisClusterConnection#randomKey(org.springframework.data.redis.connection.RedisClusterNode) + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#randomKey(org.springframework.data.redis.connection.RedisClusterNode) */ @Nullable public byte[] randomKey(RedisClusterNode node) { @@ -189,6 +183,31 @@ class LettuceClusterKeyCommands extends LettuceKeyCommands { .getValue()); } + Cursor scan(RedisClusterNode node, ScanOptions options) { + + Assert.notNull(node, "RedisClusterNode must not be null!"); + Assert.notNull(options, "Pattern must not be null!"); + + return connection.getClusterCommandExecutor() + .executeCommandOnSingleNode((LettuceClusterCommandCallback>) client -> { + + return new LettuceScanCursor(options) { + + @Override + protected LettuceScanIteration doScan(io.lettuce.core.ScanCursor cursor, ScanOptions options) { + ScanArgs scanArgs = connection.getScanArgs(options); + + KeyScanCursor keyScanCursor = client.scan(cursor, scanArgs); + List keys = keyScanCursor.getKeys(); + + return new LettuceScanIteration<>(keyScanCursor, keys); + } + + }.open(); + + }, node).getValue(); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.lettuce.LettuceConnection#sort(byte[], org.springframework.data.redis.connection.SortParameters, byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java index 2a9f88a67..7c4b1e79f 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.KeyScanCursor; import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; import io.lettuce.core.SortArgs; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; @@ -33,8 +34,6 @@ import org.springframework.data.redis.connection.RedisKeyCommands; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.Cursor; -import org.springframework.data.redis.core.ScanCursor; -import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -229,7 +228,7 @@ class LettuceKeyCommands implements RedisKeyCommands { * @return */ public Cursor scan() { - return scan(0, ScanOptions.NONE); + return scan(ScanOptions.NONE); } /* @@ -238,36 +237,31 @@ class LettuceKeyCommands implements RedisKeyCommands { */ @Override public Cursor scan(ScanOptions options) { - return scan(0, options != null ? options : ScanOptions.NONE); + return doScan(options != null ? options : ScanOptions.NONE); } /** * @since 1.4 - * @param cursorId * @param options * @return */ - public Cursor scan(long cursorId, ScanOptions options) { + private Cursor doScan(ScanOptions options) { - return new ScanCursor(cursorId, options) { + return new LettuceScanCursor(options) { - @SuppressWarnings("unchecked") @Override - protected ScanIteration doScan(long cursorId, ScanOptions options) { + protected LettuceScanIteration doScan(ScanCursor cursor, ScanOptions options) { if (isQueueing() || isPipelined()) { throw new UnsupportedOperationException("'SCAN' cannot be called in pipeline / transaction mode."); } - io.lettuce.core.ScanCursor scanCursor = connection.getScanCursor(cursorId); ScanArgs scanArgs = connection.getScanArgs(options); - KeyScanCursor keyScanCursor = getConnection().scan(scanCursor, scanArgs); - String nextCursorId = keyScanCursor.getCursor(); - + KeyScanCursor keyScanCursor = getConnection().scan(cursor, scanArgs); List keys = keyScanCursor.getKeys(); - return new ScanIteration<>(Long.valueOf(nextCursorId), (keys)); + return new LettuceScanIteration<>(keyScanCursor, keys); } @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScanCursor.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScanCursor.java new file mode 100644 index 000000000..d9f7c402e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceScanCursor.java @@ -0,0 +1,113 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.util.Collection; + +import org.springframework.data.redis.core.ScanCursor; +import org.springframework.data.redis.core.ScanIteration; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.lang.Nullable; + +/** + * Lettuce-specific {@link ScanCursor} extension that maintains the cursor state that is required for stateful-scanning + * across a Redis Cluster. + *

+ * The cursor state uses Lettuce's stateful {@link io.lettuce.core.ScanCursor} to keep track of scanning progress. + * Lettuce's cursor stores scanning progress inside its cursor so using a primitive {@code long} is not sufficient. + * + * @author Mark Paluch + * @since 2.1 + */ +abstract class LettuceScanCursor extends ScanCursor { + + @Nullable private io.lettuce.core.ScanCursor state; + + /** + * Creates a new {@link LettuceScanCursor} given {@link ScanOptions}. + * + * @param options must not be {@literal null}. + */ + LettuceScanCursor(ScanOptions options) { + super(options); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ScanCursor#doScan(long, org.springframework.data.redis.core.ScanOptions) + */ + @Override + protected ScanIteration doScan(long cursorId, ScanOptions options) { + + if (state == null && cursorId == 0) { + return scanAndProcessState(io.lettuce.core.ScanCursor.INITIAL, options); + } + + if (state != null) { + + if (isMatchingCursor(cursorId)) { + return scanAndProcessState(state, options); + } + } + + throw new IllegalArgumentException(String.format("Current scan %s state and cursor %d do not match!", + state != null ? state.getCursor() : "(none)", cursorId)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ScanCursor#isFinished(long) + */ + @Override + protected boolean isFinished(long cursorId) { + return state != null && isMatchingCursor(cursorId) ? state.isFinished() : super.isFinished(cursorId); + } + + private ScanIteration scanAndProcessState(io.lettuce.core.ScanCursor scanCursor, ScanOptions options) { + + LettuceScanIteration iteration = doScan(scanCursor, options); + state = iteration.cursor; + + return iteration; + } + + private boolean isMatchingCursor(long cursorId) { + return state != null && state.getCursor().equals(Long.toString(cursorId)); + } + + /** + * Perform the actual scan operation given {@link io.lettuce.core.ScanCursor} and {@link ScanOptions}. + * + * @param cursor must not be {@literal null}. + * @param options must not be {@literal null}. + * @return + */ + protected abstract LettuceScanIteration doScan(io.lettuce.core.ScanCursor cursor, ScanOptions options); + + /** + * Lettuce-specific extension to {@link ScanIteration} keeping track of the original + * {@link io.lettuce.core.ScanCursor} object. + */ + static class LettuceScanIteration extends ScanIteration { + + private final io.lettuce.core.ScanCursor cursor; + + LettuceScanIteration(io.lettuce.core.ScanCursor cursor, Collection items) { + super(Long.parseLong(cursor.getCursor()), items); + this.cursor = cursor; + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/ScanCursor.java b/src/main/java/org/springframework/data/redis/core/ScanCursor.java index 8d62520dc..a19660549 100644 --- a/src/main/java/org/springframework/data/redis/core/ScanCursor.java +++ b/src/main/java/org/springframework/data/redis/core/ScanCursor.java @@ -134,7 +134,7 @@ public abstract class ScanCursor implements Cursor { cursorId = Long.valueOf(result.getCursorId()); - if (cursorId == 0) { + if (isFinished(cursorId)) { state = CursorState.FINISHED; } @@ -145,6 +145,17 @@ public abstract class ScanCursor implements Cursor { } } + /** + * Check whether {@code cursorId} is finished. + * + * @param cursorId the cursor Id + * @return {@literal true} if the cursor is considered finished, {@literal false} otherwise.s + * @since 2.1 + */ + protected boolean isFinished(long cursorId) { + return cursorId == 0; + } + private void resetDelegate() { delegate = Collections. emptyList().iterator(); } diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java index ff9ef0638..baca02942 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -267,6 +267,12 @@ public interface ClusterConnectionTests { // DATAREDIS-315 void keysShouldReturnAllKeysForSpecificNode(); + // DATAREDIS-635 + void scanShouldReturnAllKeys(); + + // DATAREDIS-635 + void scanShouldReturnAllKeysForSpecificNode(); + // DATAREDIS-315 void lIndexShouldGetElementAtIndexCorrectly(); diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index 5009e28a7..1f87272f5 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -28,16 +28,7 @@ import redis.clients.jedis.JedisPool; import java.io.IOException; import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -46,6 +37,7 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; @@ -972,6 +964,31 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { assertThat(keysOnNode, not(hasItems(KEY_1_BYTES))); } + @Test(expected = InvalidDataAccessApiUsageException.class) // DATAREDIS-635 + public void scanShouldReturnAllKeys() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + clusterConnection.scan(ScanOptions.NONE); + } + + @Override // DATAREDIS-635 + public void scanShouldReturnAllKeysForSpecificNode() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + Cursor cursor = clusterConnection.scan(new RedisClusterNode("127.0.0.1", 7379, SlotRange.empty()), + ScanOptions.NONE); + + List keysOnNode = new ArrayList<>(); + cursor.forEachRemaining(keysOnNode::add); + + assertThat(keysOnNode, hasItems(KEY_2_BYTES)); + assertThat(keysOnNode, not(hasItems(KEY_1_BYTES))); + } + @Test // DATAREDIS-315 public void lIndexShouldGetElementAtIndexCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java index f2d3b5698..231c62d7d 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -977,6 +977,34 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { assertThat(keysOnNode, not(hasItems(KEY_1_BYTES))); } + @Test // DATAREDIS-635 + public void scanShouldReturnAllKeys() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + Cursor scan = clusterConnection.scan(ScanOptions.NONE); + List keys = new ArrayList<>(); + scan.forEachRemaining(keys::add); + + assertThat(keys, hasItems(KEY_1_BYTES, KEY_2_BYTES)); + } + + @Test // DATAREDIS-635 + public void scanShouldReturnAllKeysForSpecificNode() { + + nativeConnection.set(KEY_1, VALUE_1); + nativeConnection.set(KEY_2, VALUE_2); + + Cursor scan = clusterConnection.scan(new RedisClusterNode("127.0.0.1", 7379, SlotRange.empty()), + ScanOptions.NONE); + List keysOnNode = new ArrayList<>(); + scan.forEachRemaining(keysOnNode::add); + + assertThat(keysOnNode, hasItems(KEY_2_BYTES)); + assertThat(keysOnNode, not(hasItems(KEY_1_BYTES))); + } + @Test // DATAREDIS-315 public void lIndexShouldGetElementAtIndexCorrectly() {