DATAREDIS-290 - Add support for SCAN command.

SCAN is currently only supported by jedis but can be emulated for
lettuce via eval.
Srp and JRedis will respond with UnsupportedOperationException.

We provide a Cursor implementation allowing to scroll through the
responses provided by the underlying connection. The cursor will fetch
additional results from redis server whenever its starting point has not
been reached and there are no more already loaded items available.

Only values returned by the last call to redis server are kept in memory
which allows scrolling through the entire collection without potentially
running into memory issues.

Original pull request: #71.
This commit is contained in:
Christoph Strobl
2014-04-30 14:12:56 +02:00
committed by Thomas Darimont
parent 3414d2a837
commit d427928386
12 changed files with 876 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static org.springframework.data.redis.SpinBarrier.*;
import static org.springframework.data.redis.core.ScanOptions.*;
import java.util.ArrayList;
import java.util.Arrays;
@@ -41,6 +42,7 @@ import org.hamcrest.core.IsNot;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.internal.AssumptionViolatedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.RedisSystemException;
@@ -53,6 +55,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate;
import org.springframework.data.redis.connection.RedisZSetCommands.Tuple;
import org.springframework.data.redis.connection.SortParameters.Order;
import org.springframework.data.redis.connection.StringRedisConnection.StringTuple;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
@@ -1913,6 +1916,40 @@ public abstract class AbstractConnectionIntegrationTests {
assertThat(info.getDatabaseId(), is(notNullValue()));
}
/**
* @see DATAREDIS-290
*/
@Test
@IfProfileValue(name = "redisVersion", value = "2.8")
public void scanShouldReadEntireValueRange() {
if (!ConnectionUtils.isJedis(connectionFactory) && !ConnectionUtils.isLettuce(connectionFactory)) {
throw new AssumptionViolatedException("SCAN is only available for jedis and lettuce");
}
if (connection.isPipelined() || connection.isQueueing()) {
throw new AssumptionViolatedException("SCAN is only available in non pipeline | queue mode.");
}
connection.set("spring", "data");
int itemCount = 22;
for (int i = 0; i < itemCount; i++) {
connection.set(("key_" + i), ("foo_" + i));
}
Cursor<byte[]> cursor = connection.scan(scanOptions().count(20).match("ke*").build());
int i = 0;
while (cursor.hasNext()) {
byte[] value = cursor.next();
assertThat(new String(value), not(containsString("spring")));
i++;
}
assertThat(i, is(itemCount));
}
protected void verifyResults(List<Object> expected) {
assertEquals(expected, getResults());
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2014 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.core;
import static org.hamcrest.core.Is.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Stack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
/**
* @author Christoph Strobl
*/
public class ScanCursorUnitTests {
public @Rule ExpectedException exception = ExpectedException.none();
/**
* @see DATAREDIS-290
*/
@Test
public void cursorShouldNotLoopWhenNoValuesFound() {
CapturingCursorDummy cursor = initCursor(new LinkedList<ScanIteration<String>>());
assertThat(cursor.hasNext(), is(false));
}
/**
* @see DATAREDIS-290
*/
@Test(expected = NoSuchElementException.class)
public void cursorShouldReturnNullWhenNoNextElementAvailable() {
initCursor(new LinkedList<ScanIteration<String>>()).next();
}
/**
* @see DATAREDIS-290
*/
@Test
public void cursorShouldNotLoopWhenReachingStartingPointInFistLoop() {
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
values.add(createIteration(0, "spring", "data", "redis"));
CapturingCursorDummy cursor = initCursor(values);
assertThat(cursor.next(), is("spring"));
assertThat(cursor.getCursorId(), is(0L));
assertThat(cursor.hasNext(), is(true));
assertThat(cursor.next(), is("data"));
assertThat(cursor.getCursorId(), is(0L));
assertThat(cursor.hasNext(), is(true));
assertThat(cursor.next(), is("redis"));
assertThat(cursor.getCursorId(), is(0L));
assertThat(cursor.hasNext(), is(false));
}
/**
* @see DATAREDIS-290
*/
@Test
public void cursorShouldStopLoopWhenReachingStartingPoint() {
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
values.add(createIteration(1, "spring"));
values.add(createIteration(2, "data"));
values.add(createIteration(0, "redis"));
CapturingCursorDummy cursor = initCursor(values);
assertThat(cursor.next(), is("spring"));
assertThat(cursor.getCursorId(), is(1L));
assertThat(cursor.hasNext(), is(true));
assertThat(cursor.next(), is("data"));
assertThat(cursor.getCursorId(), is(2L));
assertThat(cursor.hasNext(), is(true));
assertThat(cursor.next(), is("redis"));
assertThat(cursor.getCursorId(), is(0L));
assertThat(cursor.hasNext(), is(false));
}
/**
* @see DATAREDIS-290
*/
@Test
public void shouldThrowExceptionWhenAccessingClosedCursor() {
CapturingCursorDummy cursor = new CapturingCursorDummy(null);
assertThat(cursor.isClosed(), is(true));
exception.expect(InvalidDataAccessApiUsageException.class);
exception.expectMessage("closed cursor");
cursor.next();
}
/**
* @see DATAREDIS-290
*/
@Test
public void repoeningCursorShouldHappenAtLastPosition() throws IOException {
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
values.add(createIteration(1, "spring"));
values.add(createIteration(2, "data"));
values.add(createIteration(0, "redis"));
Cursor<String> cursor = initCursor(values).open();
cursor.open();
assertThat(cursor.next(), is("spring"));
assertThat(cursor.getCursorId(), is(1L));
assertThat(cursor.hasNext(), is(true));
// close the cursor
cursor.close();
assertThat(cursor.isClosed(), is(true));
// reopen cursor at last position
cursor.open();
assertThat(cursor.next(), is("data"));
assertThat(cursor.getCursorId(), is(2L));
assertThat(cursor.hasNext(), is(true));
assertThat(cursor.next(), is("redis"));
assertThat(cursor.getCursorId(), is(0L));
assertThat(cursor.hasNext(), is(false));
}
/**
* @see DATAREDIS-290
*/
@Test
public void positionShouldBeIncrementedCorrectly() throws IOException {
LinkedList<ScanIteration<String>> values = new LinkedList<ScanIteration<String>>();
values.add(createIteration(1, "spring"));
values.add(createIteration(2, "data"));
values.add(createIteration(0, "redis"));
Cursor<String> cursor = initCursor(values).open();
cursor.open();
assertThat(cursor.getPosition(), is(0L));
cursor.next();
assertThat(cursor.getPosition(), is(1L));
cursor.next();
assertThat(cursor.getPosition(), is(2L));
}
private CapturingCursorDummy initCursor(Queue<ScanIteration<String>> values) {
CapturingCursorDummy cursor = new CapturingCursorDummy(values);
cursor.open();
return cursor;
}
private ScanIteration<String> createIteration(long cursorId, String... values) {
return new ScanIteration<String>(cursorId, Arrays.asList(values));
}
private class CapturingCursorDummy extends ScanCursor<String> {
private Queue<ScanIteration<String>> values;
private Stack<Long> cursors;
public CapturingCursorDummy(Queue<ScanIteration<String>> values) {
this.values = values;
}
@Override
protected ScanIteration<String> doScan(long cursorId, ScanOptions options) {
if (cursors == null) {
cursors = new Stack<Long>();
}
this.cursors.push(cursorId);
return this.values.poll();
}
}
}